From d3877ce75e7d6c87c7d99a51ba27234d7d6da5bb Mon Sep 17 00:00:00 2001 From: Ian MacFarland Date: Tue, 26 Jan 2021 14:32:00 -0800 Subject: [PATCH 1/3] add locality labels to content object --- .../__fixtures__/contentSource.ts | 28 ++++++++ spotlight-client/src/constants.ts | 2 + .../src/contentApi/sources/us_nd.ts | 66 +++++++++++++++++++ spotlight-client/src/contentApi/types.ts | 10 +++ .../src/contentModels/Metric.test.ts | 4 +- spotlight-client/src/contentModels/Metric.ts | 11 +++- spotlight-client/src/contentModels/Tenant.ts | 1 + .../__fixtures__/tenant_content_exhaustive.ts | 54 +++++++++++++++ .../__fixtures__/tenant_content_partial.ts | 41 ++++++++++++ .../src/contentModels/createMetricMapping.ts | 57 ++++++++++++++++ 10 files changed, 272 insertions(+), 2 deletions(-) diff --git a/spotlight-client/src/SystemNarrativePage/__fixtures__/contentSource.ts b/spotlight-client/src/SystemNarrativePage/__fixtures__/contentSource.ts index f64371ba..6f1e0d9a 100644 --- a/spotlight-client/src/SystemNarrativePage/__fixtures__/contentSource.ts +++ b/spotlight-client/src/SystemNarrativePage/__fixtures__/contentSource.ts @@ -81,6 +81,34 @@ const content: TenantContent = { ], }, }, + localities: { + Sentencing: { + label: "sentencing locality", + entries: [ + { + id: "sentencing a", + label: "sentencing A", + }, + { + id: "sentencing b", + label: "sentencing B", + }, + ], + }, + Parole: { + label: "parole locality", + entries: [ + { + id: "parole a", + label: "parole A", + }, + { + id: "parole b", + label: "parole B", + }, + ], + }, + }, }; export default content; diff --git a/spotlight-client/src/constants.ts b/spotlight-client/src/constants.ts index 792d9166..10f12349 100644 --- a/spotlight-client/src/constants.ts +++ b/spotlight-client/src/constants.ts @@ -19,6 +19,8 @@ export const ERROR_MESSAGES = { auth0Configuration: "No Auth0 configuration found.", unauthorized: "You do not have permission to view this content.", noMetricData: "Unable to retrieve valid data for this metric.", + noLocalityLabels: + "Unable to create Metric because locality labels are missing", }; export const NAV_BAR_HEIGHT = 80; diff --git a/spotlight-client/src/contentApi/sources/us_nd.ts b/spotlight-client/src/contentApi/sources/us_nd.ts index 2a455bd1..70b87504 100644 --- a/spotlight-client/src/contentApi/sources/us_nd.ts +++ b/spotlight-client/src/contentApi/sources/us_nd.ts @@ -17,6 +17,20 @@ import { TenantContent } from "../types"; +// localities for both sentencing and probation +const judicialDistricts = [ + { id: "ALL", label: "All Districts" }, + { id: "EAST_CENTRAL", label: "East Central" }, + { id: "NORTH_CENTRAL", label: "North Central" }, + { id: "NORTHEAST", label: "Northeast" }, + { id: "NORTHEAST_CENTRAL", label: "Northeast Central" }, + { id: "NORTHWEST", label: "Northwest" }, + { id: "SOUTH_CENTRAL", label: "South Central" }, + { id: "SOUTHEAST", label: "Southeast" }, + { id: "SOUTHWEST", label: "Southwest" }, + { id: "OTHER", label: "Other" }, +]; + const content: TenantContent = { name: "North Dakota", description: @@ -328,6 +342,58 @@ const content: TenantContent = { ], }, }, + localities: { + Sentencing: { + label: "Judicial District", + entries: judicialDistricts, + }, + Prison: { + label: "Facility", + entries: [ + { id: "ALL", label: "All Facilities" }, + { id: "BTC", label: "Bismarck Transition Center" }, + { id: "DWCRC", label: "Dakota Women's Correctional" }, + { id: "FTPFAR", label: "Fargo-Female Transition Program" }, + { id: "MTPFAR", label: "Fargo-Male Transition Program" }, + { id: "GFC", label: "Grand Forks County Correctional" }, + { id: "JRCC", label: "James River Correctional Center" }, + { id: "LRRP", label: "Lake Region Residential Reentry Center" }, + { id: "FTPMND", label: "Mandan-Female Transition Program" }, + { id: "MTPMND", label: "Mandan-Male Transition Program" }, + { id: "MRCC", label: "Missouri River Correctional" }, + { id: "NDSP", label: "North Dakota State Penitentiary" }, + { id: "TRC", label: "Tompkins Rehabilitation And Corrections Center" }, + ], + }, + Probation: { + label: "Judicial District", + entries: judicialDistricts, + }, + Parole: { + label: "Office", + entries: [ + { label: "All Offices", id: "ALL" }, + { label: "Beulah", id: "16" }, + { label: "Bismarck", id: "1" }, + { label: "Bottineau", id: "14" }, + { label: "Central Office", id: "17" }, + { label: "Devils Lake", id: "6" }, + { label: "Dickinson", id: "11" }, + { label: "Fargo", id: "4" }, + { label: "Grafton", id: "12" }, + { label: "Grand Forks", id: "5" }, + { label: "Jamestown", id: "2" }, + { label: "Mandan", id: "13" }, + { label: "Minot", id: "3" }, + { label: "Oakes", id: "15" }, + { label: "Rolla", id: "8" }, + { label: "Wahpeton", id: "7" }, + { label: "Washburn", id: "9" }, + { label: "Watford City", id: "18" }, + { label: "Williston", id: "10" }, + ], + }, + }, }; export default content; diff --git a/spotlight-client/src/contentApi/types.ts b/spotlight-client/src/contentApi/types.ts index 23489595..bba03717 100644 --- a/spotlight-client/src/contentApi/types.ts +++ b/spotlight-client/src/contentApi/types.ts @@ -20,6 +20,11 @@ export type NamedEntity = { description: string; }; +export type LocalityLabels = { + label: string; + entries: { id: string; label: string }[]; +}; + // ============================ // Tenant types @@ -44,6 +49,11 @@ export type TenantContent = NamedEntity & { systemNarratives: { [key in SystemNarrativeTypeId]?: SystemNarrativeContent; }; + // this is optional because it is possible (though unlikely) + // to not have any metrics that actually need it + localities?: { + [key in SystemNarrativeTypeId]?: LocalityLabels; + }; }; // ============================ diff --git a/spotlight-client/src/contentModels/Metric.test.ts b/spotlight-client/src/contentModels/Metric.test.ts index 440a76ad..e08880a7 100644 --- a/spotlight-client/src/contentModels/Metric.test.ts +++ b/spotlight-client/src/contentModels/Metric.test.ts @@ -25,10 +25,12 @@ import { reactImmediately } from "../testUtils"; import createMetricMapping from "./createMetricMapping"; const testTenantId = "US_ND"; -const testMetadataMapping = retrieveContent({ tenantId: testTenantId }).metrics; +const allTestContent = retrieveContent({ tenantId: testTenantId }); +const testMetadataMapping = allTestContent.metrics; const getTestMapping = () => createMetricMapping({ + localityLabelMapping: allTestContent.localities, metadataMapping: testMetadataMapping, tenantId: testTenantId, }); diff --git a/spotlight-client/src/contentModels/Metric.ts b/spotlight-client/src/contentModels/Metric.ts index 63478729..c62396d9 100644 --- a/spotlight-client/src/contentModels/Metric.ts +++ b/spotlight-client/src/contentModels/Metric.ts @@ -24,7 +24,7 @@ import { } from "mobx"; import { DataSeries } from "../charts/types"; import { ERROR_MESSAGES } from "../constants"; -import { TenantId } from "../contentApi/types"; +import { LocalityLabels, TenantId } from "../contentApi/types"; import { fetchMetrics, RawMetricData, @@ -45,6 +45,9 @@ export type BaseMetricConstructorOptions = { ? DemographicView : undefined; defaultLocalityId: RecordFormat extends LocalityFields ? string : undefined; + localityLabels: RecordFormat extends LocalityFields + ? LocalityLabels + : undefined; }; /** @@ -87,6 +90,10 @@ export default abstract class Metric { // filter properties localityId: RecordFormat extends LocalityFields ? string : undefined; + localityLabels: RecordFormat extends LocalityFields + ? LocalityLabels + : undefined; + demographicView: RecordFormat extends DemographicFields ? DemographicView : undefined; @@ -100,6 +107,7 @@ export default abstract class Metric { dataTransformer, defaultDemographicView, defaultLocalityId, + localityLabels, }: BaseMetricConstructorOptions) { makeObservable(this, { allRecords: observable.ref, @@ -123,6 +131,7 @@ export default abstract class Metric { // initialize filters this.localityId = defaultLocalityId; + this.localityLabels = localityLabels; this.demographicView = defaultDemographicView; } diff --git a/spotlight-client/src/contentModels/Tenant.ts b/spotlight-client/src/contentModels/Tenant.ts index f2b7e3ad..266e974d 100644 --- a/spotlight-client/src/contentModels/Tenant.ts +++ b/spotlight-client/src/contentModels/Tenant.ts @@ -81,6 +81,7 @@ function getMetricsForTenant( tenantId: TenantId ) { return createMetricMapping({ + localityLabelMapping: allTenantContent.localities, metadataMapping: allTenantContent.metrics, tenantId, }); diff --git a/spotlight-client/src/contentModels/__fixtures__/tenant_content_exhaustive.ts b/spotlight-client/src/contentModels/__fixtures__/tenant_content_exhaustive.ts index c3d3eb95..61ba8f25 100644 --- a/spotlight-client/src/contentModels/__fixtures__/tenant_content_exhaustive.ts +++ b/spotlight-client/src/contentModels/__fixtures__/tenant_content_exhaustive.ts @@ -198,6 +198,60 @@ const content: DeepRequired = { ], }, }, + localities: { + Sentencing: { + label: "sentencing locality", + entries: [ + { + id: "sentencing a", + label: "sentencing A", + }, + { + id: "sentencing b", + label: "sentencing B", + }, + ], + }, + Prison: { + label: "prison locality", + entries: [ + { + id: "prison a", + label: "prison A", + }, + { + id: "prison b", + label: "prison B", + }, + ], + }, + Probation: { + label: "probation locality", + entries: [ + { + id: "probation a", + label: "probation A", + }, + { + id: "probation b", + label: "probation B", + }, + ], + }, + Parole: { + label: "parole locality", + entries: [ + { + id: "parole a", + label: "parole A", + }, + { + id: "parole b", + label: "parole B", + }, + ], + }, + }, }; export default content; diff --git a/spotlight-client/src/contentModels/__fixtures__/tenant_content_partial.ts b/spotlight-client/src/contentModels/__fixtures__/tenant_content_partial.ts index de600b1d..8071fa54 100644 --- a/spotlight-client/src/contentModels/__fixtures__/tenant_content_partial.ts +++ b/spotlight-client/src/contentModels/__fixtures__/tenant_content_partial.ts @@ -77,6 +77,47 @@ const content: TenantContent = { ], }, }, + localities: { + Sentencing: { + label: "sentencing locality", + entries: [ + { + id: "sentencing a", + label: "sentencing A", + }, + { + id: "sentencing b", + label: "sentencing B", + }, + ], + }, + Prison: { + label: "prison locality", + entries: [ + { + id: "prison a", + label: "prison A", + }, + { + id: "prison b", + label: "prison B", + }, + ], + }, + Parole: { + label: "parole locality", + entries: [ + { + id: "parole a", + label: "parole A", + }, + { + id: "parole b", + label: "parole B", + }, + ], + }, + }, }; export default content; diff --git a/spotlight-client/src/contentModels/createMetricMapping.ts b/spotlight-client/src/contentModels/createMetricMapping.ts index 38675a06..10205a79 100644 --- a/spotlight-client/src/contentModels/createMetricMapping.ts +++ b/spotlight-client/src/contentModels/createMetricMapping.ts @@ -50,8 +50,10 @@ import SentenceTypeByLocationMetric from "./SentenceTypeByLocationMetric"; import SupervisionSuccessRateDemographicsMetric from "./SupervisionSuccessRateDemographicsMetric"; import SupervisionSuccessRateMonthlyMetric from "./SupervisionSuccessRateMonthlyMetric"; import { NOFILTER_KEY, TOTAL_KEY } from "../metricsApi/utils"; +import { ERROR_MESSAGES } from "../constants"; type MetricMappingFactoryOptions = { + localityLabelMapping: TenantContent["localities"]; metadataMapping: TenantContent["metrics"]; tenantId: TenantId; }; @@ -62,6 +64,7 @@ type MetricMappingFactoryOptions = { * type guarding on the part of consumers. */ export default function createMetricMapping({ + localityLabelMapping, metadataMapping, tenantId, }: MetricMappingFactoryOptions): MetricMapping { @@ -81,6 +84,9 @@ export default function createMetricMapping({ // allowing for 1:1 correspondence between the ID and the typed Metric instance. switch (metricType) { case "SentencePopulationCurrent": + if (!localityLabelMapping?.Sentencing) + throw new Error(ERROR_MESSAGES.noLocalityLabels); + metricMapping.set( metricType, new PopulationBreakdownByLocationMetric({ @@ -88,12 +94,16 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: NOFILTER_KEY, defaultLocalityId: NOFILTER_KEY, + localityLabels: localityLabelMapping.Sentencing, dataTransformer: sentencePopulationCurrent, sourceFileName: "sentence_type_by_district_by_demographics", }) ); break; case "SentenceTypesCurrent": + if (!localityLabelMapping?.Sentencing) + throw new Error(ERROR_MESSAGES.noLocalityLabels); + metricMapping.set( metricType, new SentenceTypeByLocationMetric({ @@ -101,12 +111,16 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: "total", defaultLocalityId: TOTAL_KEY, + localityLabels: localityLabelMapping.Sentencing, dataTransformer: sentenceTypesCurrent, sourceFileName: "sentence_type_by_district_by_demographics", }) ); break; case "PrisonPopulationCurrent": + if (!localityLabelMapping?.Prison) + throw new Error(ERROR_MESSAGES.noLocalityLabels); + metricMapping.set( metricType, new PopulationBreakdownByLocationMetric({ @@ -114,6 +128,7 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: NOFILTER_KEY, defaultLocalityId: NOFILTER_KEY, + localityLabels: localityLabelMapping.Prison, dataTransformer: prisonPopulationCurrent, sourceFileName: "incarceration_population_by_facility_by_demographics", @@ -121,6 +136,9 @@ export default function createMetricMapping({ ); break; case "ProbationPopulationCurrent": + if (!localityLabelMapping?.Probation) + throw new Error(ERROR_MESSAGES.noLocalityLabels); + metricMapping.set( metricType, new PopulationBreakdownByLocationMetric({ @@ -128,6 +146,7 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: NOFILTER_KEY, defaultLocalityId: NOFILTER_KEY, + localityLabels: localityLabelMapping.Probation, dataTransformer: probationPopulationCurrent, sourceFileName: "supervision_population_by_district_by_demographics", @@ -135,6 +154,9 @@ export default function createMetricMapping({ ); break; case "ParolePopulationCurrent": + if (!localityLabelMapping?.Parole) + throw new Error(ERROR_MESSAGES.noLocalityLabels); + metricMapping.set( metricType, new PopulationBreakdownByLocationMetric({ @@ -142,6 +164,7 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: NOFILTER_KEY, defaultLocalityId: NOFILTER_KEY, + localityLabels: localityLabelMapping.Parole, dataTransformer: parolePopulationCurrent, sourceFileName: "supervision_population_by_district_by_demographics", @@ -156,6 +179,7 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: "total", defaultLocalityId: undefined, + localityLabels: undefined, dataTransformer: prisonPopulationHistorical, sourceFileName: "incarceration_population_by_month_by_demographics", }) @@ -169,6 +193,7 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: "total", defaultLocalityId: undefined, + localityLabels: undefined, dataTransformer: probationPopulationHistorical, sourceFileName: "supervision_population_by_month_by_demographics", }) @@ -182,12 +207,16 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: "total", defaultLocalityId: undefined, + localityLabels: undefined, dataTransformer: parolePopulationHistorical, sourceFileName: "supervision_population_by_month_by_demographics", }) ); break; case "ProbationProgrammingCurrent": + if (!localityLabelMapping?.Probation) + throw new Error(ERROR_MESSAGES.noLocalityLabels); + metricMapping.set( metricType, new ProgramParticipationCurrentMetric({ @@ -195,12 +224,16 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: undefined, defaultLocalityId: NOFILTER_KEY, + localityLabels: localityLabelMapping.Probation, dataTransformer: probationProgramParticipationCurrent, sourceFileName: "active_program_participation_by_region", }) ); break; case "ParoleProgrammingCurrent": + if (!localityLabelMapping?.Parole) + throw new Error(ERROR_MESSAGES.noLocalityLabels); + metricMapping.set( metricType, new ProgramParticipationCurrentMetric({ @@ -208,12 +241,16 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: undefined, defaultLocalityId: NOFILTER_KEY, + localityLabels: localityLabelMapping.Parole, dataTransformer: paroleProgramParticipationCurrent, sourceFileName: "active_program_participation_by_region", }) ); break; case "ProbationSuccessHistorical": + if (!localityLabelMapping?.Probation) + throw new Error(ERROR_MESSAGES.noLocalityLabels); + metricMapping.set( metricType, new SupervisionSuccessRateMonthlyMetric({ @@ -221,12 +258,16 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: undefined, defaultLocalityId: TOTAL_KEY, + localityLabels: localityLabelMapping.Probation, dataTransformer: probationSuccessRateMonthly, sourceFileName: "supervision_success_by_month", }) ); break; case "ParoleSuccessHistorical": + if (!localityLabelMapping?.Parole) + throw new Error(ERROR_MESSAGES.noLocalityLabels); + metricMapping.set( metricType, new SupervisionSuccessRateMonthlyMetric({ @@ -234,12 +275,16 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: undefined, defaultLocalityId: TOTAL_KEY, + localityLabels: localityLabelMapping.Parole, dataTransformer: paroleSuccessRateMonthly, sourceFileName: "supervision_success_by_month", }) ); break; case "ProbationSuccessAggregate": + if (!localityLabelMapping?.Probation) + throw new Error(ERROR_MESSAGES.noLocalityLabels); + metricMapping.set( metricType, new SupervisionSuccessRateDemographicsMetric({ @@ -247,12 +292,16 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: "total", defaultLocalityId: TOTAL_KEY, + localityLabels: localityLabelMapping.Probation, dataTransformer: probationSuccessRateDemographics, sourceFileName: "supervision_success_by_period_by_demographics", }) ); break; case "ParoleSuccessAggregate": + if (!localityLabelMapping?.Parole) + throw new Error(ERROR_MESSAGES.noLocalityLabels); + metricMapping.set( metricType, new SupervisionSuccessRateDemographicsMetric({ @@ -260,6 +309,7 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: "total", defaultLocalityId: TOTAL_KEY, + localityLabels: localityLabelMapping.Parole, dataTransformer: paroleSuccessRateDemographics, sourceFileName: "supervision_success_by_period_by_demographics", }) @@ -273,6 +323,7 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: "total", defaultLocalityId: undefined, + localityLabels: undefined, dataTransformer: probationRevocationReasons, sourceFileName: "supervision_revocations_by_period_by_type_by_demographics", @@ -287,6 +338,7 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: "total", defaultLocalityId: undefined, + localityLabels: undefined, dataTransformer: paroleRevocationReasons, sourceFileName: "supervision_revocations_by_period_by_type_by_demographics", @@ -301,6 +353,7 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: "total", defaultLocalityId: undefined, + localityLabels: undefined, dataTransformer: prisonAdmissionReasons, sourceFileName: "incarceration_population_by_admission_reason", }) @@ -314,6 +367,7 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: "total", defaultLocalityId: undefined, + localityLabels: undefined, dataTransformer: prisonReleaseTypes, sourceFileName: "incarceration_releases_by_type_by_period", }) @@ -327,6 +381,7 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: "total", defaultLocalityId: undefined, + localityLabels: undefined, dataTransformer: recidivismRateAllFollowup, sourceFileName: "recidivism_rates_by_cohort_by_year", }) @@ -340,6 +395,7 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: "total", defaultLocalityId: undefined, + localityLabels: undefined, dataTransformer: recidivismRateConventionalFollowup, sourceFileName: "recidivism_rates_by_cohort_by_year", }) @@ -353,6 +409,7 @@ export default function createMetricMapping({ tenantId, defaultDemographicView: "total", defaultLocalityId: undefined, + localityLabels: undefined, dataTransformer: prisonStayLengths, sourceFileName: "incarceration_lengths_by_demographics", }) From 1e4b9535a7e26c37863de8295e107da336c7f6f2 Mon Sep 17 00:00:00 2001 From: Ian MacFarland Date: Tue, 26 Jan 2021 14:54:17 -0800 Subject: [PATCH 2/3] locality filter component with tests --- .../LocalityFilterSelect.test.tsx | 96 +++++++++++++++++++ .../LocalityFilterSelect.tsx | 53 ++++++++++ .../src/LocalityFilterSelect/index.ts | 18 ++++ ...istoricalPopulationBreakdownMetric.test.ts | 1 + spotlight-client/src/contentModels/Metric.ts | 1 + .../src/contentModels/createMetricMapping.ts | 8 +- 6 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 spotlight-client/src/LocalityFilterSelect/LocalityFilterSelect.test.tsx create mode 100644 spotlight-client/src/LocalityFilterSelect/LocalityFilterSelect.tsx create mode 100644 spotlight-client/src/LocalityFilterSelect/index.ts diff --git a/spotlight-client/src/LocalityFilterSelect/LocalityFilterSelect.test.tsx b/spotlight-client/src/LocalityFilterSelect/LocalityFilterSelect.test.tsx new file mode 100644 index 00000000..5fd5acf1 --- /dev/null +++ b/spotlight-client/src/LocalityFilterSelect/LocalityFilterSelect.test.tsx @@ -0,0 +1,96 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2021 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, fireEvent } from "@testing-library/react"; +import React from "react"; +import createMetricMapping from "../contentModels/createMetricMapping"; +import type PopulationBreakdownByLocationMetric from "../contentModels/PopulationBreakdownByLocationMetric"; +import contentFixture from "../contentModels/__fixtures__/tenant_content_exhaustive"; +import { reactImmediately } from "../testUtils"; +import LocalityFilterSelect from "./LocalityFilterSelect"; + +const testTenantId = "US_ND"; +const testMetricId = "ProbationPopulationCurrent"; +const testMetadataMapping = { + [testMetricId]: contentFixture.metrics[testMetricId], +}; + +// judicial districts present in backend fixture +const expectedLocalities = [ + { id: "ALL", label: "All Districts" }, + { id: "EAST_CENTRAL", label: "East Central" }, + { id: "NORTH_CENTRAL", label: "North Central" }, + { id: "NORTHEAST", label: "Northeast" }, + { id: "NORTHEAST_CENTRAL", label: "Northeast Central" }, + { id: "NORTHWEST", label: "Northwest" }, + { id: "SOUTH_CENTRAL", label: "South Central" }, + { id: "SOUTHEAST", label: "Southeast" }, + { id: "SOUTHWEST", label: "Southwest" }, + { id: "OTHER", label: "Other" }, +]; + +function getTestMetric() { + return createMetricMapping({ + localityLabelMapping: { + Probation: { label: "Judicial District", entries: expectedLocalities }, + }, + metadataMapping: testMetadataMapping, + tenantId: testTenantId, + }).get(testMetricId) as PopulationBreakdownByLocationMetric; +} + +test("has expected options", () => { + const metric = getTestMetric(); + render(); + + const menuButton = screen.getByRole("button", { + name: "Judicial District All Districts", + }); + fireEvent.click(menuButton); + + const options = screen.getAllByRole("option"); + + expect(options.length).toBe(expectedLocalities.length); + + options.forEach((option, index) => + expect(option).toHaveTextContent(expectedLocalities[index].label) + ); +}); + +test("changes demographic filter", () => { + const metric = getTestMetric(); + render(); + + const menuButton = screen.getByRole("button", { + name: "Judicial District All Districts", + }); + + expectedLocalities.forEach((expectedLocality) => { + // open the menu + fireEvent.click(menuButton); + + const option = screen.getByRole("option", { name: expectedLocality.label }); + fireEvent.click(option); + + reactImmediately(() => { + expect(metric.localityId).toBe(expectedLocality.id); + expect(menuButton).toHaveTextContent(expectedLocality.label); + }); + }); + + expect.hasAssertions(); +}); diff --git a/spotlight-client/src/LocalityFilterSelect/LocalityFilterSelect.tsx b/spotlight-client/src/LocalityFilterSelect/LocalityFilterSelect.tsx new file mode 100644 index 00000000..ac063114 --- /dev/null +++ b/spotlight-client/src/LocalityFilterSelect/LocalityFilterSelect.tsx @@ -0,0 +1,53 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2021 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 { action } from "mobx"; +import { observer } from "mobx-react-lite"; +import React from "react"; +import type PopulationBreakdownByLocationMetric from "../contentModels/PopulationBreakdownByLocationMetric"; +import type SentenceTypeByLocationMetric from "../contentModels/SentenceTypeByLocationMetric"; +import type SupervisionSuccessRateDemographicsMetric from "../contentModels/SupervisionSuccessRateDemographicsMetric"; +import type SupervisionSuccessRateMonthlyMetric from "../contentModels/SupervisionSuccessRateMonthlyMetric"; +import { Dropdown } from "../UiLibrary"; + +type LocalityFilterSelectProps = { + metric: + | PopulationBreakdownByLocationMetric + | SentenceTypeByLocationMetric + | SupervisionSuccessRateMonthlyMetric + | SupervisionSuccessRateDemographicsMetric; +}; + +const LocalityFilterSelect: React.FC = ({ + metric, +}) => { + const onChange = action("change locality filter", (newFilter: string) => { + // eslint-disable-next-line no-param-reassign + metric.localityId = newFilter; + }); + + return ( + + ); +}; + +export default observer(LocalityFilterSelect); diff --git a/spotlight-client/src/LocalityFilterSelect/index.ts b/spotlight-client/src/LocalityFilterSelect/index.ts new file mode 100644 index 00000000..353a5b02 --- /dev/null +++ b/spotlight-client/src/LocalityFilterSelect/index.ts @@ -0,0 +1,18 @@ +// Recidiviz - a data platform for criminal justice reform +// Copyright (C) 2021 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 { default } from "./LocalityFilterSelect"; diff --git a/spotlight-client/src/contentModels/HistoricalPopulationBreakdownMetric.test.ts b/spotlight-client/src/contentModels/HistoricalPopulationBreakdownMetric.test.ts index 81dc4729..5cbf8452 100644 --- a/spotlight-client/src/contentModels/HistoricalPopulationBreakdownMetric.test.ts +++ b/spotlight-client/src/contentModels/HistoricalPopulationBreakdownMetric.test.ts @@ -89,6 +89,7 @@ const getMetric = async () => { tenantId: "US_ND", defaultDemographicView: "total", defaultLocalityId: undefined, + localityLabels: undefined, dataTransformer: mockTransformer, sourceFileName: mockSourceFileName, }); diff --git a/spotlight-client/src/contentModels/Metric.ts b/spotlight-client/src/contentModels/Metric.ts index c62396d9..7a3d29e2 100644 --- a/spotlight-client/src/contentModels/Metric.ts +++ b/spotlight-client/src/contentModels/Metric.ts @@ -112,6 +112,7 @@ export default abstract class Metric { makeObservable(this, { allRecords: observable.ref, demographicView: observable, + localityId: observable, error: observable, populateAllRecords: action, isLoading: observable, diff --git a/spotlight-client/src/contentModels/createMetricMapping.ts b/spotlight-client/src/contentModels/createMetricMapping.ts index 10205a79..674825a3 100644 --- a/spotlight-client/src/contentModels/createMetricMapping.ts +++ b/spotlight-client/src/contentModels/createMetricMapping.ts @@ -93,7 +93,7 @@ export default function createMetricMapping({ ...metadata, tenantId, defaultDemographicView: NOFILTER_KEY, - defaultLocalityId: NOFILTER_KEY, + defaultLocalityId: TOTAL_KEY, localityLabels: localityLabelMapping.Sentencing, dataTransformer: sentencePopulationCurrent, sourceFileName: "sentence_type_by_district_by_demographics", @@ -127,7 +127,7 @@ export default function createMetricMapping({ ...metadata, tenantId, defaultDemographicView: NOFILTER_KEY, - defaultLocalityId: NOFILTER_KEY, + defaultLocalityId: TOTAL_KEY, localityLabels: localityLabelMapping.Prison, dataTransformer: prisonPopulationCurrent, sourceFileName: @@ -145,7 +145,7 @@ export default function createMetricMapping({ ...metadata, tenantId, defaultDemographicView: NOFILTER_KEY, - defaultLocalityId: NOFILTER_KEY, + defaultLocalityId: TOTAL_KEY, localityLabels: localityLabelMapping.Probation, dataTransformer: probationPopulationCurrent, sourceFileName: @@ -163,7 +163,7 @@ export default function createMetricMapping({ ...metadata, tenantId, defaultDemographicView: NOFILTER_KEY, - defaultLocalityId: NOFILTER_KEY, + defaultLocalityId: TOTAL_KEY, localityLabels: localityLabelMapping.Parole, dataTransformer: parolePopulationCurrent, sourceFileName: From feb2859e41b64af44689f448b46c0b2334ac8827 Mon Sep 17 00:00:00 2001 From: Ian MacFarland Date: Tue, 26 Jan 2021 18:48:30 -0800 Subject: [PATCH 3/3] fix lint error --- .../src/DemographicFilterSelect/DemographicFilterSelect.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/spotlight-client/src/DemographicFilterSelect/DemographicFilterSelect.test.tsx b/spotlight-client/src/DemographicFilterSelect/DemographicFilterSelect.test.tsx index 39bdee02..90d38c54 100644 --- a/spotlight-client/src/DemographicFilterSelect/DemographicFilterSelect.test.tsx +++ b/spotlight-client/src/DemographicFilterSelect/DemographicFilterSelect.test.tsx @@ -31,6 +31,7 @@ const testMetadataMapping = { function getTestMetric() { return createMetricMapping({ + localityLabelMapping: undefined, metadataMapping: testMetadataMapping, tenantId: testTenantId, }).get(testMetricId) as HistoricalPopulationBreakdownMetric;