diff --git a/agency-dashboard/.env.example b/agency-dashboard/.env.example
new file mode 100644
index 000000000..5009a3245
--- /dev/null
+++ b/agency-dashboard/.env.example
@@ -0,0 +1 @@
+REACT_APP_PROXY_HOST=http://localhost:5001
diff --git a/agency-dashboard/package.json b/agency-dashboard/package.json
index 22e78dce3..c8e302aa8 100644
--- a/agency-dashboard/package.json
+++ b/agency-dashboard/package.json
@@ -4,18 +4,14 @@
"private": true,
"dependencies": {
"@justice-counts/common": "*",
- "@testing-library/jest-dom": "^5.16.5",
- "@testing-library/react": "^13.4.0",
- "@testing-library/user-event": "^13.5.0",
- "@types/jest": "^27.5.2",
- "@types/node": "^16.11.64",
- "@types/react": "^18.0.21",
- "@types/react-dom": "^18.0.6",
+ "http-proxy-middleware": "^2.0.6",
+ "mobx": "^6.4.2",
+ "mobx-react-lite": "^3.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-router-dom": "^6",
"styled-components": "^5.3.6",
- "typescript": "^4.8.4",
- "web-vitals": "^2.1.4"
+ "typescript": "^4.8.4"
},
"scripts": {
"dev": "react-app-rewired start",
@@ -43,7 +39,16 @@
]
},
"devDependencies": {
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "@types/jest": "^27.5.2",
+ "@types/node": "^16.11.64",
+ "@types/react": "^18.0.21",
+ "@types/react-dom": "^18.0.6",
+ "@types/react-router-dom": "^5.3.3",
"customize-cra": "^1.0.0",
+ "jest-fetch-mock": "^3.0.3",
"react-app-rewired": "^2.2.1",
"react-scripts": "5.0.1",
"resize-observer-polyfill": "^1.5.1"
diff --git a/agency-dashboard/src/AgencyOverview.styles.tsx b/agency-dashboard/src/AgencyOverview.styles.tsx
new file mode 100644
index 000000000..a014028c0
--- /dev/null
+++ b/agency-dashboard/src/AgencyOverview.styles.tsx
@@ -0,0 +1,35 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 Recidiviz, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+// =============================================================================
+
+import { palette } from "@justice-counts/common/components/GlobalStyles";
+import styled from "styled-components/macro";
+
+export const MetricCategory = styled.div`
+ height: 100px;
+ width: 100%;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ color: ${palette.solid.blue};
+
+ &:hover {
+ cursor: pointer;
+ text-decoration: underline;
+ }
+`;
diff --git a/agency-dashboard/src/AgencyOverview.test.tsx b/agency-dashboard/src/AgencyOverview.test.tsx
new file mode 100644
index 000000000..4c4d90c30
--- /dev/null
+++ b/agency-dashboard/src/AgencyOverview.test.tsx
@@ -0,0 +1,98 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 Recidiviz, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+// =============================================================================
+import { render, screen } from "@testing-library/react";
+import React from "react";
+import { MemoryRouter } from "react-router-dom";
+
+import AgencyOverview from "./AgencyOverview";
+import { StoreProvider } from "./stores";
+
+beforeEach(() => {
+ fetchMock.resetMocks();
+});
+
+// test("renders loading state", () => {
+// fetchMock.mockResponseOnce(
+// JSON.stringify({
+// datapoints: [],
+// dimension_names_by_metric_and_disaggregation: {},
+// })
+// );
+
+// render(
+//
+//
+//
+//
+//
+// );
+// const loadingElement = screen.getByText(/Loading.../i);
+// expect(loadingElement).toBeInTheDocument();
+// });
+
+test("renders 'No published metrics' state", async () => {
+ fetchMock.mockResponseOnce(
+ JSON.stringify({
+ datapoints: [],
+ dimension_names_by_metric_and_disaggregation: {},
+ })
+ );
+
+ render(
+
+
+
+
+
+ );
+
+ const textElement = await screen.findByText(/No published metrics./i);
+ expect(textElement).toBeInTheDocument();
+});
+
+test("renders list of metrics", async () => {
+ fetchMock.mockResponseOnce(
+ JSON.stringify({
+ datapoints: [{}],
+ dimension_names_by_metric_and_disaggregation: {
+ LAW_ENFORCEMENT_ARRESTS: {},
+ LAW_ENFORCEMENT_BUDGET: {},
+ LAW_ENFORCEMENT_CALLS_FOR_SERVICE: {},
+ },
+ })
+ );
+
+ render(
+
+
+
+
+
+ );
+ const textElement1 = await screen.findByText(
+ /Click on a metric to view chart:/i
+ );
+ expect(textElement1).toBeInTheDocument();
+ const textElement2 = await screen.findByText(/LAW_ENFORCEMENT_ARRESTS/i);
+ expect(textElement2).toBeInTheDocument();
+ const textElement3 = await screen.findByText(/LAW_ENFORCEMENT_BUDGET/i);
+ expect(textElement3).toBeInTheDocument();
+ const textElement4 = await screen.findByText(
+ /LAW_ENFORCEMENT_CALLS_FOR_SERVICE/i
+ );
+ expect(textElement4).toBeInTheDocument();
+});
diff --git a/agency-dashboard/src/AgencyOverview.tsx b/agency-dashboard/src/AgencyOverview.tsx
new file mode 100644
index 000000000..28cde03b0
--- /dev/null
+++ b/agency-dashboard/src/AgencyOverview.tsx
@@ -0,0 +1,72 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 Recidiviz, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+// =============================================================================
+
+import { showToast } from "@justice-counts/common/components/Toast";
+import { observer } from "mobx-react-lite";
+import React, { useEffect } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+
+import { MetricCategory } from "./AgencyOverview.styles";
+import { useStore } from "./stores";
+
+const AgencyOverview = () => {
+ const navigate = useNavigate();
+ const params = useParams();
+ const agencyId = Number(params.id);
+ const { datapointsStore } = useStore();
+
+ const fetchDatapoints = async () => {
+ try {
+ await datapointsStore.getDatapoints(agencyId);
+ } catch (error) {
+ showToast("Error fetching data.", false, "red", 4000);
+ }
+ };
+ useEffect(() => {
+ fetchDatapoints();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ if (datapointsStore.loading) {
+ return <>Loading...>;
+ }
+
+ const metrics = Object.keys(datapointsStore.datapointsByMetric);
+ if (metrics.length === 0) {
+ return <>No published metrics.>;
+ }
+
+ return (
+ <>
+ Click on a metric to view chart:
+ {Object.keys(datapointsStore.dimensionNamesByMetricAndDisaggregation).map(
+ (metricKey) => (
+ {
+ navigate(`/agency/${agencyId}/dashboard?metric=${metricKey}`);
+ }}
+ >
+ {datapointsStore.metricKeyToDisplayName[metricKey] || metricKey}
+
+ )
+ )}
+ >
+ );
+};
+
+export default observer(AgencyOverview);
diff --git a/agency-dashboard/src/App.tsx b/agency-dashboard/src/App.tsx
index 6c026e5fa..88227a8f5 100644
--- a/agency-dashboard/src/App.tsx
+++ b/agency-dashboard/src/App.tsx
@@ -16,11 +16,18 @@
// =============================================================================
import React from "react";
+import { Route, Routes } from "react-router-dom";
-import { DashboardView } from "./DashboardView";
+import AgencyOverview from "./AgencyOverview";
+import DashboardView from "./DashboardView";
function App() {
- return ;
+ return (
+
+ } />
+ } />
+
+ );
}
export default App;
diff --git a/agency-dashboard/src/DashboardView.styles.tsx b/agency-dashboard/src/DashboardView.styles.tsx
index e6d1f3800..291b274a5 100644
--- a/agency-dashboard/src/DashboardView.styles.tsx
+++ b/agency-dashboard/src/DashboardView.styles.tsx
@@ -15,14 +15,19 @@
// along with this program. If not, see .
// =============================================================================
+import { typography } from "@justice-counts/common/components/GlobalStyles";
import styled from "styled-components/macro";
export const Container = styled.div`
- height: 100%;
+ height: 800px;
width: 100%;
- position: absolute;
+ position: relative;
display: flex;
flex-direction: column;
justify-content: stretch;
align-items: stretch;
`;
+
+export const MetricTitle = styled.div`
+ ${typography.sizeCSS.title}
+`;
diff --git a/agency-dashboard/src/DashboardView.test.tsx b/agency-dashboard/src/DashboardView.test.tsx
new file mode 100644
index 000000000..2de920ee3
--- /dev/null
+++ b/agency-dashboard/src/DashboardView.test.tsx
@@ -0,0 +1,77 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 Recidiviz, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+// =============================================================================
+import { render, screen } from "@testing-library/react";
+import React from "react";
+import { MemoryRouter } from "react-router-dom";
+
+import DashboardView from "./DashboardView";
+import { StoreProvider } from "./stores";
+
+beforeEach(() => {
+ fetchMock.resetMocks();
+});
+
+// test("renders loading state", () => {
+// fetchMock.mockResponseOnce(
+// JSON.stringify({
+// datapoints: [{}],
+// dimension_names_by_metric_and_disaggregation: {
+// LAW_ENFORCEMENT_ARRESTS: {},
+// LAW_ENFORCEMENT_BUDGET: {},
+// LAW_ENFORCEMENT_CALLS_FOR_SERVICE: {},
+// },
+// })
+// );
+
+// render(
+//
+//
+//
+//
+//
+// );
+// const loadingElement = screen.getByText(/Loading.../i);
+// expect(loadingElement).toBeInTheDocument();
+// });
+
+test("renders 'No reported data for this metric.' state", async () => {
+ fetchMock.mockResponseOnce(
+ JSON.stringify({
+ datapoints: [{}],
+ dimension_names_by_metric_and_disaggregation: {
+ LAW_ENFORCEMENT_ARRESTS: {},
+ LAW_ENFORCEMENT_BUDGET: {},
+ LAW_ENFORCEMENT_CALLS_FOR_SERVICE: {},
+ },
+ })
+ );
+
+ render(
+
+
+
+
+
+ );
+
+ const textElement = await screen.findByText(/LAW_ENFORCEMENT_ARRESTS/i);
+ expect(textElement).toBeInTheDocument();
+});
diff --git a/agency-dashboard/src/DashboardView.tsx b/agency-dashboard/src/DashboardView.tsx
index 8c5dfe023..9106e0136 100644
--- a/agency-dashboard/src/DashboardView.tsx
+++ b/agency-dashboard/src/DashboardView.tsx
@@ -16,87 +16,68 @@
// =============================================================================
import { DatapointsView } from "@justice-counts/common/components/DataViz/DatapointsView";
-import { DatapointsGroupedByAggregateAndDisaggregations } from "@justice-counts/common/types";
-import React from "react";
+import { observer } from "mobx-react-lite";
+import React, { useEffect } from "react";
+import { useLocation, useNavigate, useParams } from "react-router-dom";
-import { Container } from "./DashboardView.styles";
+import { Container, MetricTitle } from "./DashboardView.styles";
+import { useStore } from "./stores";
-const exampleDatapointsGroupedByAggregateAndDisaggregations = {
- aggregate: [
- {
- Total: 39166,
- start_date: "Wed, 01 Jan 2020 00:00:00 GMT",
- end_date: "Fri, 01 Jan 2021 00:00:00 GMT",
- frequency: "ANNUAL",
- dataVizMissingData: 0,
- },
- {
- Total: 45905,
- start_date: "Fri, 01 Jan 2021 00:00:00 GMT",
- end_date: "Sat, 01 Jan 2022 00:00:00 GMT",
- frequency: "ANNUAL",
- dataVizMissingData: 0,
- },
- {
- Total: 65836,
- start_date: "Sat, 01 Jan 2022 00:00:00 GMT",
- end_date: "Sun, 01 Jan 2023 00:00:00 GMT",
- frequency: "ANNUAL",
- dataVizMissingData: 0,
- },
- ],
- disaggregations: {
- "Correctional Facility Staff Type": {
- "Wed, 01 Jan 2020 00:00:00 GMT": {
- start_date: "Wed, 01 Jan 2020 00:00:00 GMT",
- end_date: "Fri, 01 Jan 2021 00:00:00 GMT",
- Unknown: 58386,
- frequency: "ANNUAL",
- dataVizMissingData: 0,
- Other: 61476,
- Support: 52531,
- Security: 92124,
- },
- "Fri, 01 Jan 2021 00:00:00 GMT": {
- start_date: "Fri, 01 Jan 2021 00:00:00 GMT",
- end_date: "Sat, 01 Jan 2022 00:00:00 GMT",
- Security: 10560,
- frequency: "ANNUAL",
- dataVizMissingData: 0,
- Support: 3163,
- Other: null,
- Unknown: null,
- },
- "Sat, 01 Jan 2022 00:00:00 GMT": {
- start_date: "Sat, 01 Jan 2022 00:00:00 GMT",
- end_date: "Sun, 01 Jan 2023 00:00:00 GMT",
- Other: null,
- frequency: "ANNUAL",
- dataVizMissingData: 0,
- Security: 8664,
- Support: 2975,
- Unknown: null,
- },
- },
- },
-} as DatapointsGroupedByAggregateAndDisaggregations;
+const DashboardView = () => {
+ const navigate = useNavigate();
+ const params = useParams();
+ const agencyId = Number(params.id);
+ const { datapointsStore } = useStore();
-const exampleDimensionsNamesByDisaggregations = {
- "Correctional Facility Staff Type": [
- "Security",
- "Support",
- "Other",
- "Unknown",
- ],
+ const { search } = useLocation();
+ const query = new URLSearchParams(search);
+ const metricKey = query.get("metric");
+ useEffect(() => {
+ datapointsStore.getDatapoints(agencyId);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ if (
+ metricKey &&
+ !datapointsStore.loading &&
+ !datapointsStore.dimensionNamesByMetricAndDisaggregation[metricKey]
+ ) {
+ navigate(`/agency/${agencyId}`);
+ }
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [datapointsStore.loading]);
+
+ if (
+ !metricKey ||
+ (!datapointsStore.loading &&
+ !datapointsStore.dimensionNamesByMetricAndDisaggregation[metricKey])
+ ) {
+ return null;
+ }
+
+ if (datapointsStore.loading) {
+ return <>Loading...>;
+ }
+
+ return (
+ <>
+
+
+ {datapointsStore.metricKeyToDisplayName[metricKey] || metricKey}
+
+
+
+ >
+ );
};
-export const DashboardView = () => (
-
-
-
-);
+export default observer(DashboardView);
diff --git a/agency-dashboard/src/index.tsx b/agency-dashboard/src/index.tsx
index 72d577d9b..62903064e 100644
--- a/agency-dashboard/src/index.tsx
+++ b/agency-dashboard/src/index.tsx
@@ -18,8 +18,10 @@
import { GlobalStyle } from "@justice-counts/common/components/GlobalStyles";
import React from "react";
import ReactDOM from "react-dom/client";
+import { BrowserRouter } from "react-router-dom";
import App from "./App";
+import { StoreProvider } from "./stores";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
@@ -27,6 +29,10 @@ const root = ReactDOM.createRoot(
root.render(
-
+
+
+
+
+
);
diff --git a/agency-dashboard/src/react-app-env.d.ts b/agency-dashboard/src/react-app-env.d.ts
index ac0d8866e..9daf6dc9f 100644
--- a/agency-dashboard/src/react-app-env.d.ts
+++ b/agency-dashboard/src/react-app-env.d.ts
@@ -14,3 +14,5 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
// =============================================================================
+
+///
diff --git a/agency-dashboard/src/App.test.tsx b/agency-dashboard/src/setupProxy.js
similarity index 75%
rename from agency-dashboard/src/App.test.tsx
rename to agency-dashboard/src/setupProxy.js
index 43b4caf59..8ac0f311a 100644
--- a/agency-dashboard/src/App.test.tsx
+++ b/agency-dashboard/src/setupProxy.js
@@ -14,13 +14,15 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
// =============================================================================
-import { render, screen } from "@testing-library/react";
-import React from "react";
-import App from "./App";
+const { createProxyMiddleware } = require("http-proxy-middleware");
-test("renders dashboard", () => {
- render();
- const linkElement = screen.getByText(/Date Range/i);
- expect(linkElement).toBeInTheDocument();
-});
+module.exports = function (app) {
+ app.use(
+ ["/api"],
+ createProxyMiddleware({
+ target: process.env.REACT_APP_PROXY_HOST,
+ changeOrigin: true,
+ })
+ );
+};
diff --git a/agency-dashboard/src/setupTests.ts b/agency-dashboard/src/setupTests.ts
index cda53d019..af939fd9e 100644
--- a/agency-dashboard/src/setupTests.ts
+++ b/agency-dashboard/src/setupTests.ts
@@ -16,6 +16,10 @@
// =============================================================================
import "@testing-library/jest-dom";
+import { enableFetchMocks } from "jest-fetch-mock";
+
+enableFetchMocks();
+
global.ResizeObserver = require("resize-observer-polyfill");
// polyfill for when running jest tests
diff --git a/agency-dashboard/src/stores/DatapointsStore.ts b/agency-dashboard/src/stores/DatapointsStore.ts
new file mode 100644
index 000000000..a14db0167
--- /dev/null
+++ b/agency-dashboard/src/stores/DatapointsStore.ts
@@ -0,0 +1,149 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 Recidiviz, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+// =============================================================================
+
+import {
+ DatapointsByMetric,
+ DataVizAggregateName,
+ DimensionNamesByMetricAndDisaggregation,
+ RawDatapoint,
+} from "@justice-counts/common/types";
+import { isPositiveNumber } from "@justice-counts/common/utils";
+import { makeAutoObservable, runInAction } from "mobx";
+
+import { request } from "../utils/networking";
+
+class DatapointsStore {
+ rawDatapoints: RawDatapoint[];
+
+ dimensionNamesByMetricAndDisaggregation: DimensionNamesByMetricAndDisaggregation;
+
+ loading: boolean;
+
+ constructor() {
+ makeAutoObservable(this);
+ this.rawDatapoints = [];
+ this.dimensionNamesByMetricAndDisaggregation = {};
+ this.loading = true;
+ }
+
+ get metricKeyToDisplayName(): { [metricKey: string]: string | null } {
+ const mapping: { [metricKey: string]: string | null } = {};
+ this.rawDatapoints.forEach((dp) => {
+ mapping[dp.metric_definition_key] = dp.metric_display_name;
+ });
+ return mapping;
+ }
+
+ /**
+ * Transforms raw data from the server into Datapoints keyed by metric,
+ * grouped by aggregate values and disaggregations.
+ * Aggregate is an array of objects each containing start_date, end_date, and the aggregate value.
+ * Disaggregations are keyed by disaggregation name and each value is an object
+ * with the key being the start_date and the value being an object
+ * containing start_date, end_date and key value pairs for each dimension and their values.
+ * See the DatapointsByMetric type for details.
+ */
+ get datapointsByMetric(): DatapointsByMetric {
+ return this.rawDatapoints.reduce((res: DatapointsByMetric, dp) => {
+ if (!res[dp.metric_definition_key]) {
+ res[dp.metric_definition_key] = {
+ aggregate: [],
+ disaggregations: {},
+ };
+ }
+
+ const sanitizedValue =
+ dp.value !== null && isPositiveNumber(dp.value)
+ ? Number(dp.value)
+ : null;
+
+ if (
+ dp.disaggregation_display_name === null ||
+ dp.dimension_display_name === null
+ ) {
+ res[dp.metric_definition_key].aggregate.push({
+ [DataVizAggregateName]: sanitizedValue,
+ start_date: dp.start_date,
+ end_date: dp.end_date,
+ frequency: dp.frequency,
+ dataVizMissingData: 0,
+ });
+ } else {
+ if (
+ !res[dp.metric_definition_key].disaggregations[
+ dp.disaggregation_display_name
+ ]
+ ) {
+ res[dp.metric_definition_key].disaggregations[
+ dp.disaggregation_display_name
+ ] = {};
+ }
+ res[dp.metric_definition_key].disaggregations[
+ dp.disaggregation_display_name
+ ][dp.start_date] = {
+ ...res[dp.metric_definition_key].disaggregations[
+ dp.disaggregation_display_name
+ ][dp.start_date],
+ start_date: dp.start_date,
+ end_date: dp.end_date,
+ [dp.dimension_display_name]: sanitizedValue,
+ frequency: dp.frequency,
+ dataVizMissingData: 0,
+ };
+ }
+ return res;
+ }, {});
+ }
+
+ async getDatapoints(agencyId: number): Promise {
+ try {
+ const response = (await request({
+ path: `/api/agencies/${agencyId}/published_datapoints`,
+ method: "GET",
+ })) as Response;
+ if (response.status === 200) {
+ const result = await response.json();
+ runInAction(() => {
+ this.rawDatapoints = result.datapoints;
+ this.dimensionNamesByMetricAndDisaggregation =
+ result.dimension_names_by_metric_and_disaggregation;
+ });
+ } else {
+ const error = await response.json();
+ throw new Error(error.description);
+ }
+ runInAction(() => {
+ this.loading = false;
+ });
+ } catch (error) {
+ runInAction(() => {
+ this.loading = false;
+ });
+ throw error;
+ }
+ }
+
+ resetState() {
+ // reset the state
+ runInAction(() => {
+ this.rawDatapoints = [];
+ this.loading = true;
+ });
+ }
+}
+
+export default DatapointsStore;
diff --git a/agency-dashboard/src/stores/RootStore.ts b/agency-dashboard/src/stores/RootStore.ts
new file mode 100644
index 000000000..00b0eb602
--- /dev/null
+++ b/agency-dashboard/src/stores/RootStore.ts
@@ -0,0 +1,29 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 Recidiviz, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+// =============================================================================
+import DatapointsStore from "./DatapointsStore";
+
+class RootStore {
+ datapointsStore: DatapointsStore;
+
+ constructor() {
+ this.datapointsStore = new DatapointsStore();
+ }
+}
+
+export default new RootStore();
+
+export type { RootStore };
diff --git a/agency-dashboard/src/stores/StoreProvider.tsx b/agency-dashboard/src/stores/StoreProvider.tsx
new file mode 100644
index 000000000..404401c62
--- /dev/null
+++ b/agency-dashboard/src/stores/StoreProvider.tsx
@@ -0,0 +1,42 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 Recidiviz, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+// =============================================================================
+
+import React, { useContext } from "react";
+
+import rootStore from "./RootStore";
+
+const StoreContext = React.createContext(
+ undefined
+);
+
+export const StoreProvider: React.FC = ({
+ children,
+}): React.ReactElement => {
+ return (
+ {children}
+ );
+};
+
+export function useStore(): typeof rootStore {
+ const context = useContext(StoreContext);
+
+ if (context === undefined) {
+ throw new Error("useStore must be used within a StoreProvider");
+ }
+
+ return context;
+}
diff --git a/agency-dashboard/src/stores/index.ts b/agency-dashboard/src/stores/index.ts
new file mode 100644
index 000000000..0534b7850
--- /dev/null
+++ b/agency-dashboard/src/stores/index.ts
@@ -0,0 +1,20 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 Recidiviz, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+// =============================================================================
+
+export * from "./DatapointsStore";
+export { default as rootStore } from "./RootStore";
+export * from "./StoreProvider";
diff --git a/agency-dashboard/src/utils/networking.ts b/agency-dashboard/src/utils/networking.ts
new file mode 100644
index 000000000..d096309a4
--- /dev/null
+++ b/agency-dashboard/src/utils/networking.ts
@@ -0,0 +1,46 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 Recidiviz, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+// =============================================================================
+
+interface RequestProps {
+ path: string;
+ method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
+ body?: FormData | Record;
+}
+
+export const request = async ({
+ path,
+ method,
+ body,
+}: RequestProps): Promise => {
+ // Files are sent as FormData and not JSON
+ const jsonOrFormDataBody =
+ body instanceof FormData ? body : JSON.stringify(body);
+
+ const headers: HeadersInit = {};
+
+ if (!(body instanceof FormData)) {
+ headers["Content-Type"] = "application/json";
+ }
+
+ const response = await fetch(path, {
+ body: method !== "GET" ? jsonOrFormDataBody : null,
+ method,
+ headers,
+ });
+
+ return response;
+};
diff --git a/publisher/src/components/assets/status-check-white-icon.png b/common/assets/status-check-white-icon.png
similarity index 100%
rename from publisher/src/components/assets/status-check-white-icon.png
rename to common/assets/status-check-white-icon.png
diff --git a/publisher/src/components/Toast/Toast.ts b/common/components/Toast/Toast.ts
similarity index 98%
rename from publisher/src/components/Toast/Toast.ts
rename to common/components/Toast/Toast.ts
index 15fae52b5..d6e5a7bd9 100644
--- a/publisher/src/components/Toast/Toast.ts
+++ b/common/components/Toast/Toast.ts
@@ -21,7 +21,7 @@ import {
typography,
} from "@justice-counts/common/components/GlobalStyles";
-import checkIconWhite from "../assets/status-check-white-icon.png";
+import checkIconWhite from "../../assets/status-check-white-icon.png";
type ToastColor = "blue" | "red" | "grey";
diff --git a/publisher/src/components/Toast/index.ts b/common/components/Toast/index.ts
similarity index 100%
rename from publisher/src/components/Toast/index.ts
rename to common/components/Toast/index.ts
diff --git a/common/utils/index copy.ts b/common/index.d.ts
similarity index 89%
rename from common/utils/index copy.ts
rename to common/index.d.ts
index 3d8962b73..4d7a74852 100644
--- a/common/utils/index copy.ts
+++ b/common/index.d.ts
@@ -15,6 +15,4 @@
// along with this program. If not, see .
// =============================================================================
-export * from "./conversionUtils";
-export * from "./dateUtils";
-export * from "./helperUtils";
+declare module "*.png";
diff --git a/common/tsconfig.json b/common/tsconfig.json
index c37853f2e..b640d44b5 100644
--- a/common/tsconfig.json
+++ b/common/tsconfig.json
@@ -1,3 +1,4 @@
{
- "extends": "@recidiviz/tsconfig/react"
+ "extends": "@recidiviz/tsconfig/react",
+ "include": ["index.d.ts"] /// <-- Like this!!
}
diff --git a/publisher/package.json b/publisher/package.json
index 137612a34..aee9b2c5d 100644
--- a/publisher/package.json
+++ b/publisher/package.json
@@ -22,8 +22,7 @@
"react-router-dom": "^6",
"recharts": "^2.1.13",
"styled-components": "^5.3.6",
- "typescript": "^4.4.2",
- "web-vitals": "^2.1.0"
+ "typescript": "^4.4.2"
},
"scripts": {
"dev": "react-app-rewired start",
diff --git a/publisher/src/components/DataUpload/DataUpload.tsx b/publisher/src/components/DataUpload/DataUpload.tsx
index f33d25d8c..1a982db80 100644
--- a/publisher/src/components/DataUpload/DataUpload.tsx
+++ b/publisher/src/components/DataUpload/DataUpload.tsx
@@ -15,6 +15,7 @@
// along with this program. If not, see .
// =============================================================================
+import { showToast } from "@justice-counts/common/components/Toast";
import { AgencySystems } from "@justice-counts/common/types";
import { observer } from "mobx-react-lite";
import React, { useEffect, useState } from "react";
@@ -24,7 +25,6 @@ import { useStore } from "../../stores";
import logoImg from "../assets/jc-logo-vector.png";
import { Logo, LogoContainer } from "../Header";
import { Loader } from "../Loading";
-import { showToast } from "../Toast";
import {
Button,
DataUploadContainer,
diff --git a/publisher/src/components/DataUpload/UploadFile.tsx b/publisher/src/components/DataUpload/UploadFile.tsx
index d43d6d3ca..b766915d3 100644
--- a/publisher/src/components/DataUpload/UploadFile.tsx
+++ b/publisher/src/components/DataUpload/UploadFile.tsx
@@ -15,12 +15,12 @@
// along with this program. If not, see .
// =============================================================================
+import { showToast } from "@justice-counts/common/components/Toast";
import { AgencySystems } from "@justice-counts/common/types";
import React, { Fragment, useEffect, useRef, useState } from "react";
import { removeSnakeCase } from "../../utils";
import { ReactComponent as FileIcon } from "../assets/file-icon.svg";
-import { showToast } from "../Toast";
import {
DragDropContainer,
GeneralInstructions,
diff --git a/publisher/src/components/DataUpload/UploadedFiles.tsx b/publisher/src/components/DataUpload/UploadedFiles.tsx
index 131185c04..f50eca860 100644
--- a/publisher/src/components/DataUpload/UploadedFiles.tsx
+++ b/publisher/src/components/DataUpload/UploadedFiles.tsx
@@ -15,6 +15,7 @@
// along with this program. If not, see .
// =============================================================================
+import { showToast } from "@justice-counts/common/components/Toast";
import { Permission } from "@justice-counts/common/types";
import { when } from "mobx";
import { observer } from "mobx-react-lite";
@@ -25,7 +26,6 @@ import { removeSnakeCase } from "../../utils";
import downloadIcon from "../assets/download-icon.png";
import { Badge, BadgeColorMapping, BadgeColors } from "../Badge";
import { Loader } from "../Loading";
-import { showToast } from "../Toast";
import {
ActionButton,
ActionsContainer,
diff --git a/publisher/src/components/MetricsView/MetricsView.tsx b/publisher/src/components/MetricsView/MetricsView.tsx
index 3d756030a..a298aa8c9 100644
--- a/publisher/src/components/MetricsView/MetricsView.tsx
+++ b/publisher/src/components/MetricsView/MetricsView.tsx
@@ -15,6 +15,7 @@
// along with this program. If not, see .
// =============================================================================
+import { showToast } from "@justice-counts/common/components/Toast";
import {
AgencySystems,
FormError,
@@ -43,7 +44,6 @@ import {
} from "../Forms";
import { Loading } from "../Loading";
import { PageTitle, TabbedBar, TabbedItem, TabbedOptions } from "../Reports";
-import { showToast } from "../Toast";
import {
ActiveMetricSettingHeader,
Dimension,
diff --git a/publisher/src/components/Onboarding/Onboarding.tsx b/publisher/src/components/Onboarding/Onboarding.tsx
index 48179b0bd..564c6f56d 100644
--- a/publisher/src/components/Onboarding/Onboarding.tsx
+++ b/publisher/src/components/Onboarding/Onboarding.tsx
@@ -19,6 +19,7 @@ import {
palette,
typography,
} from "@justice-counts/common/components/GlobalStyles";
+import { showToast } from "@justice-counts/common/components/Toast";
import React, { useEffect, useRef, useState } from "react";
import styled, { keyframes } from "styled-components/macro";
@@ -30,7 +31,6 @@ import {
SIDE_PANEL_WIDTH,
TWO_PANEL_MAX_WIDTH,
} from "../Reports/ReportDataEntry.styles";
-import { showToast } from "../Toast";
export const OnboardingContainer = styled.div`
width: 100vw;
diff --git a/publisher/src/components/Reports/CreateReport.tsx b/publisher/src/components/Reports/CreateReport.tsx
index e5bb40b8f..a9e5d3b95 100644
--- a/publisher/src/components/Reports/CreateReport.tsx
+++ b/publisher/src/components/Reports/CreateReport.tsx
@@ -19,6 +19,7 @@ import {
palette,
typography,
} from "@justice-counts/common/components/GlobalStyles";
+import { showToast } from "@justice-counts/common/components/Toast";
import {
CreateReportFormValuesType,
ReportOverview,
@@ -45,7 +46,6 @@ import {
TitleWrapper,
} from "../Forms";
import { Dropdown } from "../Forms/Dropdown";
-import { showToast } from "../Toast";
import {
PublishButton,
PublishDataWrapper,
diff --git a/publisher/src/components/Reports/DataEntryForm.tsx b/publisher/src/components/Reports/DataEntryForm.tsx
index 9343825b0..08edde940 100644
--- a/publisher/src/components/Reports/DataEntryForm.tsx
+++ b/publisher/src/components/Reports/DataEntryForm.tsx
@@ -20,6 +20,7 @@ import {
palette,
typography,
} from "@justice-counts/common/components/GlobalStyles";
+import { showToast } from "@justice-counts/common/components/Toast";
import { reaction, runInAction } from "mobx";
import { observer } from "mobx-react-lite";
import React, { Fragment, useEffect, useRef, useState } from "react";
@@ -56,7 +57,6 @@ import {
Title,
} from "../Forms";
import { Onboarding, OnboardingDataEntrySummary } from "../Onboarding";
-import { showToast } from "../Toast";
import {
AdditionalContextInput,
BinaryRadioButtonInputs,
diff --git a/publisher/src/components/Reports/PublishConfirmation.tsx b/publisher/src/components/Reports/PublishConfirmation.tsx
index c31a95327..dd0cff86b 100644
--- a/publisher/src/components/Reports/PublishConfirmation.tsx
+++ b/publisher/src/components/Reports/PublishConfirmation.tsx
@@ -16,6 +16,7 @@
// =============================================================================
import { palette } from "@justice-counts/common/components/GlobalStyles";
+import { showToast } from "@justice-counts/common/components/Toast";
import {
MetricContextWithErrors,
MetricDisaggregationDimensionsWithErrors,
@@ -32,7 +33,6 @@ import { useStore } from "../../stores";
import { printReportTitle, rem } from "../../utils";
import errorIcon from "../assets/status-error-icon.png";
import { Button } from "../Forms";
-import { showToast } from "../Toast";
import { PublishButton } from "./ReportDataEntry.styles";
const CONTAINER_WIDTH = 912;
diff --git a/publisher/src/components/Reports/ReportDataEntry.tsx b/publisher/src/components/Reports/ReportDataEntry.tsx
index b01081395..eaabd9daf 100644
--- a/publisher/src/components/Reports/ReportDataEntry.tsx
+++ b/publisher/src/components/Reports/ReportDataEntry.tsx
@@ -15,6 +15,7 @@
// along with this program. If not, see .
// =============================================================================
+import { showToast } from "@justice-counts/common/components/Toast";
import { Report } from "@justice-counts/common/types";
import { when } from "mobx";
import { observer } from "mobx-react-lite";
@@ -26,7 +27,6 @@ import { useStore } from "../../stores";
import { printReportTitle } from "../../utils";
import { PageWrapper } from "../Forms";
import { Loading } from "../Loading";
-import { showToast } from "../Toast";
import DataEntryForm from "./DataEntryForm";
import PublishConfirmation from "./PublishConfirmation";
import PublishDataPanel from "./PublishDataPanel";
diff --git a/publisher/src/stores/API.ts b/publisher/src/stores/API.ts
index 8f67b89d4..ebef14124 100644
--- a/publisher/src/stores/API.ts
+++ b/publisher/src/stores/API.ts
@@ -15,11 +15,11 @@
// along with this program. If not, see .
// =============================================================================
+import { showToast } from "@justice-counts/common/components/Toast";
import { makeAutoObservable, runInAction, when } from "mobx";
import { trackLoadTime, trackNetworkError } from "../analytics";
import { AuthStore } from "../components/Auth";
-import { showToast } from "../components/Toast";
export interface RequestProps {
path: string;
diff --git a/publisher/src/stores/DatapointsStore.ts b/publisher/src/stores/DatapointsStore.ts
index 5b9e9fb19..bdd78d202 100644
--- a/publisher/src/stores/DatapointsStore.ts
+++ b/publisher/src/stores/DatapointsStore.ts
@@ -165,6 +165,9 @@ class DatapointsStore {
const error = await response.json();
throw new Error(error.description);
}
+ runInAction(() => {
+ this.loading = false;
+ });
} catch (error) {
runInAction(() => {
this.loading = false;
diff --git a/publisher/src/stores/UserStore.ts b/publisher/src/stores/UserStore.ts
index 71219903e..9e55d1768 100644
--- a/publisher/src/stores/UserStore.ts
+++ b/publisher/src/stores/UserStore.ts
@@ -14,12 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
// =============================================================================
+import { showToast } from "@justice-counts/common/components/Toast";
import { UserAgency } from "@justice-counts/common/types";
import { makeAutoObservable, runInAction, when } from "mobx";
import { makePersistable } from "mobx-persist-store";
import { APP_METADATA_CLAIM, AuthStore } from "../components/Auth";
-import { showToast } from "../components/Toast";
import API from "./API";
type UserSettingsRequestBody = {
diff --git a/yarn.lock b/yarn.lock
index cba05187a..0d6168439 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10351,11 +10351,6 @@ wbuf@^1.1.0, wbuf@^1.7.3:
dependencies:
minimalistic-assert "^1.0.0"
-web-vitals@^2.1.0, web-vitals@^2.1.4:
- version "2.1.4"
- resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.4.tgz#76563175a475a5e835264d373704f9dde718290c"
- integrity sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==
-
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"