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

Till/2400 message banner #2421

Merged
merged 15 commits into from
Sep 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ jobs:
- name: Run linter
run: make lint-ui

- name: Run tests
run: make test-ui

- name: Build production image
run: docker build -t alephdata/aleph-ui-production:${GITHUB_SHA} -f ui/Dockerfile.production ui

Expand Down
9 changes: 9 additions & 0 deletions aleph.env.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ ALEPH_APP_TITLE=Aleph
ALEPH_APP_NAME=aleph
ALEPH_UI_URL=http://localhost:8080/

# Set a static, app-wide message banner displayed at the top of every page.
# This can be useful to inform users about planned downtime etc.
# ALEPH_APP_BANNER="This is an app-wide message."

# Instead of defining a static message using the `ALEPH_APP_BANNER` variable,
# you can also define a JSON endpoint that Aleph will use to fetch app-wide
# messages to display at the top of every page.
# ALEPH_APP_MESSAGES_URL=https://example.org/messages.json

# ALEPH_URL_SCHEME=https
# ALEPH_FAVICON=https://investigativedashboard.org/static/favicon.ico
# ALEPH_LOGO=http://assets.pudo.org/img/logo_bigger.png
Expand Down
1 change: 1 addition & 0 deletions aleph/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

# Show a system-wide banner in the user interface.
APP_BANNER = env.get("ALEPH_APP_BANNER")
APP_MESSAGES_URL = env.get("ALEPH_APP_MESSAGES_URL", None)

# Force HTTPS here:
FORCE_HTTPS = True if APP_UI_URL.lower().startswith("https") else False
Expand Down
1 change: 1 addition & 0 deletions aleph/views/base_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def _metadata_locale(locale):
"version": __version__,
"banner": settings.APP_BANNER,
"ui_uri": settings.APP_UI_URL,
"messages_url": settings.APP_MESSAGES_URL,
Comment on lines 53 to +54
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we prefer? URI or URL?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL is fine, it seems consistent with the settings portion of the line above, which isn't actually consistent with itself, hmmm.

"publish": archive.can_publish,
"logo": app_logo,
"favicon": settings.APP_FAVICON,
Expand Down
5 changes: 5 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"@formatjs/intl-pluralrules": "^5.0.2",
"@formatjs/intl-relativetimeformat": "^11.0.1",
"@formatjs/intl-utils": "^3.8.4",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^12.1.5",
"@types/jest": "^28.1.4",
"@types/node": "^17.0.8",
"@types/react": "^17.0.1",
Expand Down Expand Up @@ -72,6 +74,9 @@
"eslintConfig": {
"extends": "react-app"
},
"jest": {
"transformIgnorePatterns": []
},
"browserslist": {
"production": [
">0.2%",
Expand Down
1 change: 1 addition & 0 deletions ui/src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export {
fetchProfileTags,
pairwiseJudgement,
} from './profileActions';
export { fetchMessages } from './messagesActions';
export {
fetchMetadata,
fetchStatistics,
Expand Down
10 changes: 10 additions & 0 deletions ui/src/actions/messagesActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import axios from 'axios';
import asyncActionCreator from 'actions/asyncActionCreator';

export const fetchMessages = asyncActionCreator(
(endpoint) => async () => {
const response = await axios.get(endpoint);
return { messages: response.data };
},
{ name: 'FETCH_MESSAGES' }
);
1 change: 0 additions & 1 deletion ui/src/app/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ body {
margin: 0;
padding: 0;
font-family: $pt-font-family;
font-weight: 300;
font-size: $aleph-font-size;

display: flex;
Expand Down
54 changes: 49 additions & 5 deletions ui/src/app/Router.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import { Route, Navigate, Routes } from 'react-router-dom';
import { connect } from 'react-redux';
import { Spinner } from '@blueprintjs/core';

import { fetchMetadata } from 'actions';
import { selectSession, selectMetadata } from 'selectors';
import { fetchMetadata, fetchMessages } from 'actions';
import {
selectSession,
selectMetadata,
selectMessages,
selectPinnedMessage,
} from 'selectors';
import Navbar from 'components/Navbar/Navbar';
import MessageBanner from 'components/MessageBanner/MessageBanner';

import NotFoundScreen from 'screens/NotFoundScreen/NotFoundScreen';
import OAuthScreen from 'screens/OAuthScreen/OAuthScreen';
Expand Down Expand Up @@ -34,24 +40,55 @@ import ExportsScreen from 'src/screens/ExportsScreen/ExportsScreen';

import './Router.scss';

const MESSAGES_INTERVAL = 15 * 60 * 1000; // every 15 minutes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a check every 15 minutes? What are the implications of a shorter/longer interval?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refreshing the messages more frequently seemed a little wasteful. The worst case scenario here is that users will see an update 15 minutes after it has been published, which seemed acceptable to me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I noticed when I was playing with the message banner earlier in the week was that updates to the UI were inconsistent. I reduced the refresh to 60 seconds and even accounting for the build process is was really hit and miss as to when the update would show. I also couldn't figure out how long the "fixed" green banner would persist?


class Router extends Component {
componentDidMount() {
this.fetchIfNeeded();
this.setMessagesInterval();
}

componentDidUpdate() {
this.fetchIfNeeded();
}

componentWillUnmount() {
this.clearMessagesInterval();
}

fetchIfNeeded() {
const { metadata } = this.props;
const { metadata, messages } = this.props;

if (metadata.shouldLoad) {
this.props.fetchMetadata();
}

if (messages.shouldLoad) {
this.fetchMessages();
}
}

fetchMessages() {
const { metadata } = this.props;

if (metadata?.app?.messages_url) {
this.props.fetchMessages(metadata.app.messages_url);
}
}

setMessagesInterval() {
const id = setInterval(() => this.fetchMessages(), MESSAGES_INTERVAL);
this.setState(() => ({ messagesInterval: id }));
}

clearMessagesInterval() {
if (this.state?.messagesInterval) {
clearInterval(this.state.messagesInterval);
}
}

render() {
const { metadata, session } = this.props;
const { metadata, session, pinnedMessage } = this.props;
const isLoaded = metadata && metadata.app && session;

const Loading = (
Expand All @@ -61,13 +98,15 @@ class Router extends Component {
</div>
</div>
);

if (!isLoaded) {
return Loading;
}

return (
<>
<Navbar />
<MessageBanner message={pinnedMessage} />
<Suspense fallback={Loading}>
<Routes>
<Route path="oauth" element={<OAuthScreen />} />
Expand Down Expand Up @@ -160,7 +199,12 @@ class Router extends Component {

const mapStateToProps = (state) => ({
metadata: selectMetadata(state),
messages: selectMessages(state),
pinnedMessage: selectPinnedMessage(state),
session: selectSession(state),
});

export default connect(mapStateToProps, { fetchMetadata })(Router);
export default connect(mapStateToProps, {
fetchMetadata,
fetchMessages,
})(Router);
20 changes: 14 additions & 6 deletions ui/src/components/Dashboard/Dashboard.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
@import 'app/variables.scss';
@import 'app/mixins.scss';

$dashboard-padding: $aleph-grid-size * 2.5;

.Dashboard {
padding: $dashboard-padding;
padding: $aleph-content-padding;

&__inner-container {
display: flex;
Expand All @@ -24,8 +22,18 @@ $dashboard-padding: $aleph-grid-size * 2.5;
1px solid $aleph-border-color,
null
);
@include rtlSupportInvertedProp(margin, right, $dashboard-padding, null);
@include rtlSupportInvertedProp(padding, right, $dashboard-padding, null);
@include rtlSupportInvertedProp(
margin,
right,
$aleph-content-padding,
null
);
@include rtlSupportInvertedProp(
padding,
right,
$aleph-content-padding,
null
);
min-width: 235px;

@media screen and (max-width: $aleph-screen-sm-max-width) {
Expand Down Expand Up @@ -76,7 +84,7 @@ $dashboard-padding: $aleph-grid-size * 2.5;
}

&__title-container {
margin: $dashboard-padding 0;
margin: $aleph-content-padding 0;
}

&__title {
Expand Down
17 changes: 2 additions & 15 deletions ui/src/components/Entity/EntityHeading.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { selectUnit } from '@formatjs/intl-utils';
import { FormattedRelativeTime } from 'react-intl';
import { Entity, Schema } from 'components/common';
import { Entity, Schema, RelativeTime } from 'components/common';

import 'components/common/ItemOverview.scss';

Expand All @@ -12,7 +10,6 @@ class EntityHeading extends React.PureComponent {
const lastViewedDate = entity.lastViewed
? new Date(parseInt(entity.lastViewed, 10))
: Date.now();
const { value, unit } = selectUnit(lastViewedDate, Date.now());

return (
<>
Expand All @@ -36,17 +33,7 @@ class EntityHeading extends React.PureComponent {
<FormattedMessage
id="entity.info.last_view"
defaultMessage="Last viewed {time}"
values={{
time: (
<FormattedRelativeTime
value={value}
unit={unit}
// eslint-disable-next-line
style="long"
numeric="auto"
/>
),
}}
values={{ time: <RelativeTime date={lastViewedDate} /> }}
/>
</span>
)}
Expand Down
20 changes: 7 additions & 13 deletions ui/src/components/Exports/Export.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { PureComponent } from 'react';
import { selectUnit } from '@formatjs/intl-utils';
import { FormattedRelativeTime } from 'react-intl';
import c from 'classnames';

import { Skeleton, ExportLink, FileSize } from 'src/components/common';
import convertUTCDateToLocalDate from 'util/convertUTCDateToLocalDate';
import {
Skeleton,
ExportLink,
FileSize,
RelativeTime,
} from 'src/components/common';

import './Export.scss';

Expand Down Expand Up @@ -35,8 +37,6 @@ class Export extends PureComponent {

const { id, expires_at: expiresAt, export_status: status } = export_;

const expiryDate = convertUTCDateToLocalDate(new Date(expiresAt));
const { value, unit } = selectUnit(expiryDate);
return (
<tr key={id} className={c('Export nowrap', status)}>
<td className="export-label wide">
Expand All @@ -47,13 +47,7 @@ class Export extends PureComponent {
</td>
<td className="export-status">{export_.status}</td>
<td className="timestamp">
<FormattedRelativeTime
value={value}
unit={unit}
// eslint-disable-next-line
style="long"
numeric="auto"
/>
<RelativeTime utcDate={expiresAt} />
</td>
</tr>
);
Expand Down
59 changes: 59 additions & 0 deletions ui/src/components/MessageBanner/MessageBanner.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Callout, Intent, Classes } from '@blueprintjs/core';
import { injectIntl } from 'react-intl';
import { RelativeTime } from 'components/common';

import './MessageBanner.scss';

const MESSAGE_INTENTS = {
info: Intent.PRIMARY,
warning: Intent.WARNING,
error: Intent.DANGER,
success: Intent.SUCCESS,
};

function Wrapper({ children }) {
return (
<div className="MessageBanner" role="status" aria-atomic="true">
{children}
</div>
);
}

function MessageBanner({ message }) {
if (!message) {
return <Wrapper />;
}

const intent = MESSAGE_INTENTS[message.level] || Intent.WARNING;
const updates = message.updates || [];

const latestUpdate =
updates.length > 0 ? updates[updates.length - 1] : message;

return (
<Wrapper>
<Callout intent={intent} icon={null} className="MessageBanner__callout">
<p>
{message.title && (
<>
<strong className={Classes.HEADING}>{message.title}</strong>
<br />
</>
)}

<span
dangerouslySetInnerHTML={{ __html: latestUpdate.safeHtmlBody }}
/>

{latestUpdate.createdAt && (
<span className="MessageBanner__meta">
<RelativeTime date={latestUpdate.createdAt} />
</span>
)}
</p>
</Callout>
</Wrapper>
);
}

export default injectIntl(MessageBanner);
24 changes: 24 additions & 0 deletions ui/src/components/MessageBanner/MessageBanner.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@import '../../app/variables.scss';

.MessageBanner__callout.MessageBanner__callout {
padding: 0.5 * $aleph-content-padding $aleph-content-padding;
}

.MessageBanner p {
margin: 0;
}

.MessageBanner a,
.MessageBanner a:hover {
color: inherit;
text-decoration: underline;
font-weight: 600;
}

.MessageBanner__meta {
opacity: 0.6;
}

.MessageBanner__meta::before {
content: ' — ';
}
Loading