Skip to content

Commit cce3076

Browse files
Merge pull request #479 from adhocteam/main
TTA overview widget (#316)
2 parents 928fca5 + 33c8924 commit cce3076

39 files changed

+1321
-197
lines changed

.circleci/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ parameters:
164164
default: "main"
165165
type: string
166166
sandbox_git_branch: # change to feature branch to test deployment
167-
default: "kw-adjust-grantee-job"
167+
default: "kw-overview-widget"
168168
type: string
169169
prod_new_relic_app_id:
170170
default: "877570491"

frontend/.env

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ BACKEND_PROXY=http://localhost:8080
22
REACT_APP_INACTIVE_MODAL_TIMEOUT=1500000
33
REACT_APP_SESSION_TIMEOUT=1800000
44
REACT_APP_TTA_SMART_HUB_URI=http://localhost:3000
5+
REACT_APP_ENABLE_WIDGETS=true

frontend/src/App.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import Home from './pages/Home';
1919
import Landing from './pages/Landing';
2020
import ActivityReport from './pages/ActivityReport';
2121
import LegacyReport from './pages/LegacyReport';
22+
import Widgets from './pages/Widgets';
23+
2224
import isAdmin from './permissions';
2325
import 'react-dates/initialize';
2426
import 'react-dates/lib/css/_datepicker.css';
@@ -76,6 +78,7 @@ function App() {
7678
}
7779

7880
const admin = isAdmin(user);
81+
const enableWidgets = process.env.REACT_APP_ENABLE_WIDGETS === 'true';
7982

8083
const renderAuthenticatedRoutes = () => (
8184
<div role="main" id="main-content">
@@ -113,7 +116,14 @@ function App() {
113116
<ActivityReport location={location} match={match} user={user} />
114117
)}
115118
/>
116-
119+
{enableWidgets && (
120+
<Route
121+
path="/widgets"
122+
render={() => (
123+
<Widgets />
124+
)}
125+
/>
126+
)}
117127
{admin && (
118128
<Route
119129
path="/admin"

frontend/src/__tests__/permissions.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ describe('permissions', () => {
5656
},
5757
],
5858
};
59-
const regions = allRegionsUserHasPermissionTo(user);
59+
const includeAdmin = true;
60+
const regions = allRegionsUserHasPermissionTo(user, includeAdmin);
6061
expect(regions).toEqual(expect.arrayContaining([14, 3, 4]));
6162
});
6263

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.smart-hub--region-dropdown {
2+
background-color: #0166AB;
3+
border: none;
4+
border-radius: 5px;
5+
color: #FFFFFF;
6+
padding-left: 18px;
7+
font-weight: 700;
8+
font-size: 17px;
9+
display:inline-block;
10+
background-image: url(../images/triange_down.png);
11+
}
12+
13+
.smart-hub--region-dropdown option{
14+
background-color: white;
15+
border: none;
16+
border-radius: 5px;
17+
padding: 0px;
18+
}
19+
20+
/* .arrow-down {
21+
border-left:5px solid rgba(0,0,0,0);
22+
border-right:5px solid rgba(0,0,0,0);
23+
color: #FFFFFF;
24+
} */

frontend/src/fetchers/Widgets.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import join from 'url-join';
2+
import { get } from './index';
3+
4+
const fetchWidget = async (widgetId, region, query = '') => {
5+
const queryStr = query ? `?${query}&` : '?&';
6+
const res = await get(join('/', 'api', 'widgets', `${widgetId}${queryStr}region.in[]=${region}`));
7+
return res.json();
8+
};
9+
10+
export default fetchWidget;

frontend/src/images/check.svg

+1
Loading

frontend/src/images/triange_down.png

298 Bytes
Loading

frontend/src/pages/Landing/Filter.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
DATE_FMT,
1515
QUERY_CONDITIONS,
1616
} from './constants';
17+
import { DECIMAL_BASE } from '../../Constants';
1718
import './Filter.css';
1819

1920
const defaultFilter = () => (
@@ -186,7 +187,7 @@ Filter.defaultProps = {
186187
forMyAlerts: false,
187188
};
188189

189-
export function filtersToQueryString(filters) {
190+
export function filtersToQueryString(filters, region) {
190191
const filtersWithValues = filters.filter((f) => {
191192
if (f.condition === WITHIN) {
192193
const [startDate, endDate] = f.query.split('-');
@@ -198,6 +199,9 @@ export function filtersToQueryString(filters) {
198199
const con = QUERY_CONDITIONS[filter.condition];
199200
return `${filter.topic}.${con}=${filter.query}`;
200201
});
202+
if (region && (parseInt(region, DECIMAL_BASE) !== -1)) {
203+
queryFragments.push(`region.in[]=${parseInt(region, DECIMAL_BASE)}`);
204+
}
201205
return queryFragments.join('&');
202206
}
203207

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/* eslint-disable react/forbid-prop-types */
2+
/* eslint-disable react/jsx-props-no-spreading */
3+
import React, { useState } from 'react';
4+
import PropTypes from 'prop-types';
5+
import Select, { components } from 'react-select';
6+
import { Button } from '@trussworks/react-uswds';
7+
8+
import 'uswds/dist/css/uswds.css';
9+
import '@trussworks/react-uswds/lib/index.css';
10+
import './index.css';
11+
12+
import triangleDown from '../../images/triange_down.png';
13+
import check from '../../images/check.svg';
14+
15+
const DropdownIndicator = (props) => (
16+
<components.DropdownIndicator {...props}>
17+
<img alt="" style={{ width: '22px' }} src={triangleDown} />
18+
</components.DropdownIndicator>
19+
);
20+
21+
const Placeholder = (props) => <components.Placeholder {...props} />;
22+
23+
export const getUserOptions = (regions) => regions.map((region) => ({ value: region, label: `Region ${region}` }));
24+
25+
const styles = {
26+
container: (provided, state) => {
27+
// To match the focus indicator provided by uswds
28+
const outline = state.isFocused ? '0.25rem solid #2491ff;' : '';
29+
return {
30+
...provided,
31+
outline,
32+
};
33+
},
34+
input: () => ({ display: 'none' }),
35+
control: (provided) => ({
36+
...provided,
37+
borderColor: '#0166AB',
38+
backgroundColor: '#0166AB',
39+
borderRadius: '5px',
40+
paddingLeft: '5px',
41+
paddingTop: '4px',
42+
paddingBottom: '4px',
43+
whiteSpace: 'nowrap',
44+
color: 'white',
45+
width: '120px',
46+
}),
47+
indicatorSeparator: () => ({ display: 'none' }),
48+
menu: (provided) => ({
49+
...provided,
50+
width: '200px',
51+
}),
52+
option: (provided, state) => ({
53+
...provided,
54+
color: state.isSelected ? '#0166AB' : 'black',
55+
fontWeight: state.isSelected ? '700' : 'normal',
56+
backgroundColor: state.isSelected ? '#F8F8F8' : '#FFFFFF',
57+
padding: 11,
58+
}),
59+
singleValue: (provided) => {
60+
const single = { color: '#FFFFFF', fontWeight: 600 };
61+
62+
return {
63+
...provided, ...single,
64+
};
65+
},
66+
valueContainer: () => ({ padding: '10px 8px' }),
67+
};
68+
69+
function RegionalSelect(props) {
70+
const {
71+
regions, onApply,
72+
} = props;
73+
74+
const [selectedItem, setSelectedItem] = useState();
75+
const [appliedItem, setAppliedItem] = useState();
76+
const [menuIsOpen, setMenuIsOpen] = useState(false);
77+
78+
// const delayedCloseMenu = () => setTimeout(setMenuIsOpen(false), 1000);
79+
80+
const CustomOption = (customOptionProps) => {
81+
const {
82+
data, innerRef, innerProps, isSelected,
83+
} = customOptionProps;
84+
return data.custom ? (
85+
<div ref={innerRef} {...innerProps}>
86+
<Button
87+
type="button"
88+
className="float-left margin-2 smart-hub--filter-button"
89+
onClick={() => {
90+
onApply(selectedItem);
91+
setAppliedItem(selectedItem);
92+
setMenuIsOpen(false);
93+
}}
94+
>
95+
Apply
96+
</Button>
97+
</div>
98+
) : (
99+
<components.Option {...customOptionProps}>
100+
{data.label}
101+
{isSelected && (
102+
<img
103+
className="tta-smarthub--check"
104+
src={check}
105+
style={{
106+
width: 32,
107+
float: 'right',
108+
marginTop: '-9px ',
109+
}}
110+
alt={data.label}
111+
/>
112+
)}
113+
</components.Option>
114+
);
115+
};
116+
117+
CustomOption.propTypes = {
118+
data: PropTypes.object.isRequired,
119+
innerRef: PropTypes.string.isRequired,
120+
innerProps: PropTypes.object.isRequired,
121+
};
122+
123+
const options = [...getUserOptions(regions), { custom: true }];
124+
return (
125+
<Select
126+
options={options}
127+
menuIsOpen={menuIsOpen}
128+
onChange={(value) => { if (value && value.value) setSelectedItem(value); }}
129+
onMenuOpen={() => setMenuIsOpen(true)}
130+
onBlur={() => setMenuIsOpen(false)}
131+
// onBlur={() => delayedCloseMenu()}
132+
name="RegionalSelect"
133+
defaultValue={options[0]}
134+
value={{
135+
value: selectedItem ? selectedItem.value : options[0].value,
136+
label: appliedItem ? appliedItem.label : options[0].label,
137+
}}
138+
styles={styles}
139+
components={{ Placeholder, DropdownIndicator, Option: CustomOption }}
140+
placeholder="Select Region"
141+
closeMenuOnSelect={false}
142+
maxMenuHeight={600}
143+
/>
144+
);
145+
}
146+
147+
RegionalSelect.propTypes = {
148+
regions: PropTypes.arrayOf(PropTypes.number).isRequired,
149+
onApply: PropTypes.func.isRequired,
150+
};
151+
152+
export default RegionalSelect;

frontend/src/pages/Landing/__tests__/MyAlerts.js

+5
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ const renderMyAlerts = () => {
1919
const alertsPerPage = ALERTS_PER_PAGE;
2020
const alertsActivePage = 1;
2121
const alertReportsCount = 10;
22+
const updateReportAlerts = jest.fn();
23+
const setAlertReportsCount = jest.fn();
2224
const requestAlertsSort = jest.fn();
25+
2326
render(
2427
<Router history={history}>
2528
<MyAlerts
@@ -31,6 +34,8 @@ const renderMyAlerts = () => {
3134
alertsActivePage={alertsActivePage}
3235
alertReportsCount={alertReportsCount}
3336
sortHandler={requestAlertsSort}
37+
updateReportAlerts={updateReportAlerts}
38+
setAlertReportsCount={setAlertReportsCount}
3439
fetchReports={() => {}}
3540
updateReportFilters={() => {}}
3641
handleDownloadAllAlerts={() => {}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import '@testing-library/jest-dom';
2+
import React from 'react';
3+
import {
4+
render, screen, fireEvent,
5+
} from '@testing-library/react';
6+
import { Router } from 'react-router';
7+
import { createMemoryHistory } from 'history';
8+
9+
import selectEvent from 'react-select-event';
10+
import RegionalSelect from '../RegionalSelect';
11+
12+
const renderRegionalSelect = () => {
13+
const history = createMemoryHistory();
14+
const onApplyRegion = jest.fn();
15+
16+
render(
17+
<Router history={history}>
18+
<RegionalSelect
19+
regions={[1, 2]}
20+
onApply={onApplyRegion}
21+
/>
22+
</Router>,
23+
);
24+
return history;
25+
};
26+
27+
describe('Regional Select', () => {
28+
test('displays correct region in input', async () => {
29+
renderRegionalSelect();
30+
const input = await screen.findByText(/region 1/i);
31+
expect(input).toBeVisible();
32+
});
33+
34+
test('changes input value on apply', async () => {
35+
renderRegionalSelect();
36+
let input = await screen.findByText(/region 1/i);
37+
expect(input).toBeVisible();
38+
await selectEvent.select(screen.getByText(/region 1/i), [/region 2/i]);
39+
const applyButton = await screen.findByText(/apply/i);
40+
fireEvent.click(applyButton);
41+
input = await screen.findByText(/region 2/i);
42+
expect(input).toBeVisible();
43+
});
44+
});

0 commit comments

Comments
 (0)