Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Next Release #2316

Merged
merged 11 commits into from
Jun 9, 2022
Merged
2 changes: 1 addition & 1 deletion helm/charts/aleph/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ ingestfile:

image:
repository: ghcr.io/alephdata/ingest-file
tag: "3.16.1"
tag: "3.16.3"
pullPolicy: Always

containerSecurityContext:
Expand Down
10 changes: 5 additions & 5 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Dependencies maintained by OCCRP
banal==1.0.6
followthemoney==2.9.3
followthemoney==2.9.5
followthemoney-store[postgresql]==3.0.3
followthemoney-compare==0.4.3
fingerprints==1.0.3
Expand All @@ -16,21 +16,21 @@ Flask-Migrate==3.1.0
Flask-Cors==3.0.10
Flask-Babel==2.0.0
flask-talisman==1.0.0
SQLAlchemy==1.4.36
alembic==1.7.7
SQLAlchemy==1.4.37
alembic==1.8.0
authlib==0.15.5

elasticsearch==7.17.0
marshmallow==2.19.2
gunicorn[eventlet]==20.1.0
jsonschema==4.5.1
jsonschema==4.6.0
apispec==5.2.2
apispec-webframeworks==0.5.2
blinker==1.4
Babel==2.10.1
PyYAML==5.4.1
python-frontmatter==1.0.0
pyjwt >= 2.0.1, < 2.4.0
pyjwt >= 2.0.1, < 2.5.0
cryptography >= 36.0.0, < 38.0.0
requests[security] >= 2.25.1, < 3.0.0
urllib3==1.26.9
Expand Down
7 changes: 4 additions & 3 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"version": "3.12.3",
"private": true,
"dependencies": {
"@alephdata/followthemoney": "2.9.3",
"@alephdata/react-ftm": "2.6.0",
"@alephdata/followthemoney": "2.9.4",
"@alephdata/react-ftm": "2.6.5",
"@blueprintjs/colors": "^3.0.0",
"@blueprintjs/core": "3.54.0",
"@blueprintjs/icons": "3.31.0",
Expand All @@ -14,7 +14,7 @@
"@formatjs/cli": "^4.2.2",
"@formatjs/intl-locale": "^2.4.14",
"@formatjs/intl-pluralrules": "^4.0.6",
"@formatjs/intl-relativetimeformat": "^10.0.1",
"@formatjs/intl-relativetimeformat": "^11.0.1",
"@formatjs/intl-utils": "^3.8.4",
"@types/jest": "^27.0.1",
"@types/node": "^17.0.8",
Expand All @@ -26,6 +26,7 @@
"js-file-download": "^0.4.9",
"jwt-decode": "^3.0.0",
"lodash": "^4.17.11",
"moment": "^2.29.1",
"node-sass": "6.0.1",
"numeral": "^2.0.6",
"papaparse": "^5.1.0",
Expand Down
107 changes: 92 additions & 15 deletions ui/src/components/Facet/DateFacet.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React, { Component } from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { defineMessages, FormattedMessage, FormattedDate, injectIntl } from 'react-intl';
import queryString from 'query-string';
import { Button, Card, Icon, Intent, Spinner } from '@blueprintjs/core';
import { Histogram } from '@alephdata/react-ftm';
import moment from 'moment';

import withRouter from 'app/withRouter'
import { DEFAULT_START_INTERVAL, filterDateIntervals, formatDateQParam, timestampToYear } from 'components/Facet/util';

import { DEFAULT_START_INTERVAL, filterDateIntervals, formatDateQParam, timestampToLabel, isDateIntervalUncertain } from 'components/Facet/util';
import { selectEntitiesResult, selectLocale } from 'selectors'

import './DateFacet.scss';

Expand All @@ -19,6 +20,18 @@ const messages = defineMessages({
id: 'search.screen.dates_label',
defaultMessage: 'results',
},
uncertainMonth: {
id: 'search.screen.dates_uncertain_month',
defaultMessage: '* this count includes dates in {year} where no month is specified',
},
uncertainDay: {
id: 'search.screen.dates_uncertain_day',
defaultMessage: '* this count includes dates where no day is specified',
},
uncertainDayMonth: {
id: 'search.screen.dates_uncertain_day_month',
defaultMessage: '* this count includes dates in {year} where no day or month is specified',
},
});

export class DateFilter extends Component {
Expand All @@ -29,16 +42,28 @@ export class DateFilter extends Component {
}

onSelect(selected) {
const { field, query, updateQuery } = this.props;
const { facetInterval, field, query, updateQuery } = this.props;

let newRange;
let newQuery = query;

if (Array.isArray(selected)) {
newRange = selected.sort().map(formatDateQParam);
newRange = selected.sort().map(val => formatDateQParam(val, facetInterval));
} else {
const year = formatDateQParam(selected);
newRange = [year, year];

if (facetInterval === 'year') {
newQuery = newQuery.set(`facet_interval:${field}`, 'month')
const end = moment.utc(selected).endOf('year').format('YYYY-MM-DD')
newRange = [formatDateQParam(selected, 'month'), formatDateQParam(end, 'month')]
} else if (facetInterval === 'month') {
newQuery = newQuery.set(`facet_interval:${field}`, 'day')
const end = moment.utc(selected).endOf('month').format('YYYY-MM-DD')
newRange = [formatDateQParam(selected, 'day'), formatDateQParam(end, 'day')]
} else {
newRange = [formatDateQParam(selected, 'day'), formatDateQParam(selected, 'day')]
}
}
const newQuery = query.setFilter(`gte:${field}`, newRange[0])
newQuery = newQuery.setFilter(`gte:${field}`, newRange[0])
.setFilter(`lte:${field}`, newRange[1]);

updateQuery(newQuery)
Expand Down Expand Up @@ -88,26 +113,74 @@ export class DateFilter extends Component {
);
}

renderParentLabel() {
const { facetInterval, filteredIntervals } = this.props;

const sampleDate = filteredIntervals[0].label

const content = facetInterval === 'month'
? moment.utc(sampleDate).year()
: (
<FormattedDate
value={sampleDate}
year="numeric"
month="long"
/>
)
return <span className="DateFacet__parent-label">{content}</span>
}

formatUncertainWarning(timestamp) {
const { facetInterval, intl } = this.props;
const year = moment.utc(timestamp).year()

if (facetInterval === 'month') {
return intl.formatMessage(messages.uncertainMonth, { year })
} else {
const isFirstMonth = moment.utc(timestamp).month() === 0
return intl.formatMessage(messages[isFirstMonth ? 'uncertainDayMonth' : 'uncertainDay'], { year });
}
}

formatData(dataPropName) {
const { facetInterval, filteredIntervals, locale } = this.props;

return filteredIntervals.map(({ label, count, id }) => {
const isUncertain = facetInterval !== 'year' && isDateIntervalUncertain(label, facetInterval)
const uncertainWarning = isUncertain && this.formatUncertainWarning(label)

return ({
...timestampToLabel(label, facetInterval, locale),
[dataPropName]: count,
isUncertain,
uncertainWarning,
id
})
})
}

render() {
const { dataLabel, emptyComponent, filteredIntervals, intl, displayShowHiddenToggle, showAll, showLabel = true } = this.props;
const { dataLabel, emptyComponent, facetInterval, filteredIntervals, intl, displayShowHiddenToggle, showLabel = true } = this.props;
let content;

if (filteredIntervals) {
if (!filteredIntervals.length) {
content = emptyComponent;
} else {
const dataPropName = dataLabel || intl.formatMessage(messages.results);

content = (
<>
{facetInterval !== 'year' && this.renderParentLabel()}
<Histogram
data={filteredIntervals.map(({ label, count, ...rest }) => ({ label: timestampToYear(label), [dataPropName]: count, ...rest }))}
data={this.formatData(dataPropName)}
dataPropName={dataPropName}
onSelect={this.onSelect}
containerProps={{
height: DATE_FACET_HEIGHT,
}}
/>
{(displayShowHiddenToggle || showAll) && this.renderShowHiddenToggle()}
{displayShowHiddenToggle && this.renderShowHiddenToggle()}
</>
)
}
Expand Down Expand Up @@ -137,17 +210,21 @@ export class DateFilter extends Component {
}

const mapStateToProps = (state, ownProps) => {
const { location, intervals, query } = ownProps;
const { field, location, intervals, query } = ownProps;
const hashQuery = queryString.parse(location.hash);

const showAll = hashQuery.show_all_dates === 'true';
const result = selectEntitiesResult(state, query);

if (intervals) {
const { filteredIntervals, hasOutOfRange } = filterDateIntervals({ query, intervals, useDefaultBounds: !showAll })
if (intervals && !result.isPending) {
const { filteredIntervals, hasOutOfRange } = filterDateIntervals({ field, query, intervals, useDefaultBounds: !showAll })
const locale = selectLocale(state);
return {
filteredIntervals,
displayShowHiddenToggle: hasOutOfRange,
showAll
facetInterval: query.getString(`facet_interval:${field}`),
showAll,
locale: locale === 'en' ? 'en-gb' : locale
};
}
return {};
Expand Down
6 changes: 6 additions & 0 deletions ui/src/components/Facet/DateFacet.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@
}
}

&__parent-label {
font-weight: bold;
color: $blue2;
margin-bottom: $aleph-grid-size/2;
}

.ErrorSection {
min-height: unset;

Expand Down
3 changes: 2 additions & 1 deletion ui/src/components/Facet/Facet.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ class Facet extends Component {
const { field, query } = this.props;
const newQuery = query
.clearFilter(`lte:${field}`)
.clearFilter(`gte:${field}`);
.clearFilter(`gte:${field}`)
.set(`facet_interval:${field}`, 'year')

this.props.updateQuery(newQuery);
}
Expand Down
84 changes: 64 additions & 20 deletions ui/src/components/Facet/util.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,98 @@
const DEFAULT_START_INTERVAL = 1950;
const ES_SUFFIX = '||/y';
import moment from 'moment';

const formatDateQParam = (datetime) => {
return `${new Date(datetime).getFullYear()}||/y`
const DEFAULT_START_INTERVAL = '1950';

const formatDateQParam = (datetime, granularity) => {
const date = moment.utc(datetime).format("YYYY-MM-DD")
if (granularity === 'month') {
return `${date}||/M`
} else if (granularity === 'day') {
return `${date}||/d`
}
return `${date}||/y`
};

const cleanDateQParam = (value) => {
return value.replace(ES_SUFFIX, '');
if (!value) { return; }
const [date, suffix] = value.split('||/');

if (suffix === 'y') {
return moment.utc(date).format('YYYY')
} else if (suffix === 'M') {
return moment.utc(date).format('YYYY-MM')
} else {
return date;
}
};

const timestampToYear = timestamp => {
return new Date(timestamp).getFullYear();
const timestampToLabel = (timestamp, granularity, locale) => {
const dateObj = new Date(timestamp)
let label, tooltipLabel;

if (granularity === 'month') {
label = new Intl.DateTimeFormat(locale, { month: 'short' }).format(dateObj)
tooltipLabel = new Intl.DateTimeFormat(locale, { month: 'short', year: 'numeric' }).format(dateObj)
} else if (granularity === 'day') {
label = dateObj.getDate()
tooltipLabel = new Intl.DateTimeFormat(locale, { month: 'short', year: 'numeric', day: 'numeric' }).format(dateObj)
} else {
label = tooltipLabel = dateObj.getFullYear();
}

return ({ label, tooltipLabel })
}

const filterDateIntervals = ({ query, intervals, useDefaultBounds }) => {
const defaultEndInterval = new Date().getFullYear();
const hasGtFilter = query.hasFilter('gte:dates');
const hasLtFilter = query.hasFilter('lte:dates');
const filterDateIntervals = ({ field, query, intervals, useDefaultBounds }) => {
const defaultEndInterval = moment.utc().format('YYYY-MM-DD');
const hasGtFilter = query.hasFilter(`gte:${field}`);
const hasLtFilter = query.hasFilter(`lte:${field}`);

const gt = hasGtFilter
? cleanDateQParam(query.getFilter('gte:dates')[0])
const gtRaw = hasGtFilter
? cleanDateQParam(query.getFilter(`gte:${field}`)[0])
: (useDefaultBounds && DEFAULT_START_INTERVAL);

const lt = hasLtFilter
? cleanDateQParam(query.getFilter('lte:dates')[0])
const ltRaw = hasLtFilter
? cleanDateQParam(query.getFilter(`lte:${field}`)[0])
: (useDefaultBounds && defaultEndInterval);

let gtOutOfRange, ltOutOfRange = false;
const gt = gtRaw && moment(gtRaw)
const lt = ltRaw && moment(ltRaw)

let gtOutOfRange, ltOutOfRange = false;
const filteredIntervals = intervals.filter(({ id }) => {
const year = timestampToYear(id);
if (gt && year < gt) {
if (gt && gt.isAfter(id)) {
gtOutOfRange = true;
return false;
}
if (lt && year > lt) {
if (lt && lt.isBefore(id)) {
ltOutOfRange = true;
return false;
}
return true;
})

const hasOutOfRange = useDefaultBounds && ((!hasGtFilter && gtOutOfRange) || (!hasLtFilter && ltOutOfRange));

return { filteredIntervals, hasOutOfRange };
}

const isDateIntervalUncertain = (timestamp, granularity) => {
const dateObj = moment.utc(timestamp)

if (granularity === 'month' && dateObj.month() === 0) {
return true;
} else if (granularity === 'day' && dateObj.date() === 1) {
return true;
}

return false;
}

export {
cleanDateQParam,
DEFAULT_START_INTERVAL,
formatDateQParam,
timestampToYear,
timestampToLabel,
isDateIntervalUncertain,
filterDateIntervals
}
Loading