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

Add UI directive, progress bar, and status counts #56

Merged
merged 21 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 18 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: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
},
"ignorePatterns": [
"**/node_modules/**",
"**/dist/**"
"**/dist/**",
"**/static/**"
],
"plugins": ["promise", "node"],
"rules": {
Expand Down
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{
"type": "node",
"request": "launch",
"name": "Launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
Expand Down
23 changes: 23 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@types/node": "^20.8.8",
"@types/uuid": "^9.0.6",
"axios": "^1.5.1",
"axios-retry": "^4.0.0",
"csv": "^6.3.5",
"dotenv": "^16.3.1",
"fastify": "^4.23.2",
Expand Down
49 changes: 49 additions & 0 deletions scripts/retry-logic-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const express = require('express');

const app = express();

// Middleware function to set timeout for all POST requests
const timeoutMiddleware = (req, res, next) => {
const TIMEOUT_DURATION = 1000;

req.setTimeout(TIMEOUT_DURATION, () => {
// Handle timeout
res.status(408).send('Request Timeout');
});

next();
};

app.post('/_session', (req, res) => {
res.set('Set-Cookie', 'AuthSession=abc123');
res.status(200).send('OK');
});

app.get('/medic/_design/medic-client/_view/contacts_by_type_freetext', (req, res) => {
const startkey = JSON.parse(req.query.startkey);
console.log('contacts_by_type_freetext', startkey);
const DATA = [
// eslint-disable-next-line max-len
{ id: 'e847f6e2-6dba-46dd-8128-5b153d0cd75f', key: ['b_sub_county', 'name:malava'], value: 'false false b_sub_county malava', doc: { _id: 'e847f6e2-6dba-46dd-8128-5b153d0cd75f', _rev: '1-cd20b7095c20172237867233b0375eda', parent: { _id: '95d9abd1-7c17-41b1-af98-595509f96631' }, type: 'contact', is_name_generated: 'false', name: 'Malava', external_id: '', contact: { _id: '1e3d8375-6ab4-4409-be3f-3324db7658e9' }, contact_type: 'b_sub_county', reported_date: 1702573623984 } },
// eslint-disable-next-line max-len
{ id: '2926bf4c-63eb-433d-a2b4-274fd05d2f1c', key: ['c_community_health_unit', 'name:chu'], value: 'false false c_community_health_unit chu', doc: { _id: '2926bf4c-63eb-433d-a2b4-274fd05d2f1c', _rev: '1-c15f26fe064f8357c19d1124286bf4c4', name: 'Chu', PARENT: 'Chepalungu', code: '123456', type: 'contact', contact_type: 'c_community_health_unit', parent: { _id: 'e847f6e2-6dba-46dd-8128-5b153d0cd75f', parent: { _id: '95d9abd1-7c17-41b1-af98-595509f96631' } }, contact: { _id: 'bb9ebc4c6af161ee0f53b42339001fb1' }, reported_date: 1701631255451 } },
];
res.json({
total_rows: 2,
offset: 0,
rows: DATA.filter(r => r.key[0] === startkey[0])
});
});

app.use(timeoutMiddleware);

app.all('*', (req, res) => {
setTimeout(() => {
res.status(200).send('OK');
}, 2000);
});

// Start the server
app.listen(3556, () => {
console.log(`Server is listening on port ${3556}`);
});
10 changes: 9 additions & 1 deletion src/lib/cht-api.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import _ from 'lodash';
import axios, { AxiosHeaders } from 'axios';
import axiosRetry from 'axios-retry';
import { axiosRetryConfig } from './retry-logic';
import { UserPayload } from '../services/user-payload';
import { AuthenticationInfo, Config, ContactType } from '../config';

axiosRetry(axios, axiosRetryConfig);

const {
NODE_ENV
} = process.env;
Expand Down Expand Up @@ -164,7 +168,11 @@ export class ChtApi {
createUser = async (user: UserPayload): Promise<void> => {
const url = `${this.protocolAndHost}/api/v1/users`;
console.log('axios.post', url);
await axios.post(url, user, this.authorizationOptions());
const axiosRequestionConfig = {
...this.authorizationOptions(),
'axios-retry': { retries: 0 }, // upload-manager handles retries for this
};
await axios.post(url, user, axiosRequestionConfig);
};

getParentAndSibling = async (parentId: string, contactType: ContactType): Promise<{ parent: any; sibling: any }> => {
Expand Down
70 changes: 70 additions & 0 deletions src/lib/retry-logic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { AxiosError, AxiosRequestConfig } from 'axios';
import isRetryAllowed from 'is-retry-allowed';
import { UserPayload } from '../services/user-payload';
import { ChtApi } from './cht-api';

const RETRY_COUNT = 4;

export const axiosRetryConfig = {
retries: RETRY_COUNT,
retryDelay: () => 1000,
retryCondition: (err: AxiosError) => {
const status = err.response?.status;
return (!status || status >= 500) && isRetryAllowed(err);
},
onRetry: (retryCount: number, error: AxiosError, requestConfig: AxiosRequestConfig) => {
console.log(`${requestConfig.url} failure. Retrying (${retryCount})`);
},
};

export async function retryOnUpdateConflict<T>(funcWithPut: () => Promise<T>): Promise<T> {
for (let retryCount = 0; retryCount < RETRY_COUNT; retryCount++) {
try {
return await funcWithPut();
} catch (err : any) {
const statusCode = err.response?.status;
if (statusCode === 409) {
console.log(`Retrying on update-conflict (${retryCount})`);
continue;
}

throw err;
}
}

throw Error('update-conflict 409 persisted');
}

export async function createUserWithRetries(userPayload: UserPayload, chtApi: ChtApi): Promise<{ username: string; password: string }> {
for (let retryCount = 0; retryCount < RETRY_COUNT; ++retryCount) {
try {
await chtApi.createUser(userPayload);
return userPayload;
} catch (err: any) {
if (axiosRetryConfig.retryCondition(err)) {
continue;
}

if (err.response?.status !== 400) {
throw err;
}

const translationKey = err.response?.data?.error?.translationKey;
console.error('createUser retry because', translationKey);
if (translationKey === 'username.taken') {
userPayload.makeUsernameMoreComplex();
continue;
}

const RETRY_PASSWORD_TRANSLATIONS = ['password.length.minimum', 'password.weak'];
if (RETRY_PASSWORD_TRANSLATIONS.includes(translationKey)) {
userPayload.regeneratePassword();
continue;
}

throw err;
}
}

throw new Error('could not create user ' + userPayload.contact);
}
15 changes: 10 additions & 5 deletions src/public/app/fragment_home.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
<div class="tile is-child" hx-ext="sse" sse-connect="/events/connection">
<div id="places_parent" class="tile is-child" hx-get="/events/places_list" hx-target="#place_tables"
hx-swap="innerHTML" hx-trigger="sse:places_state_change">
<div id="place_tables">
{% include "place/list.html" %}
</div>
<div
id="places_parent"
class="tile is-child"
hx-get="/events/places/all"
hx-target="this"
hx-swap="none"
hx-trigger="sse:place_state_change"
>
{% include "place/directive.html" %}
{% include "place/list.html" %}
</div>
</div>
11 changes: 3 additions & 8 deletions src/public/app/nav.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,12 @@
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link is-arrowless">
<span class="material-symbols-outlined">cloud</span>
Connected to <i>{{ session.authInfo.friendly}} as {{ session.username }}</i>
Connected to <i>{{ session.authInfo.friendly }} as {{ session.username }}</i>
</a>

<div class="navbar-dropdown is-right">
<a class="navbar-item" hx-post="/app/apply-changes" hx-swap="none">
<span class="material-symbols-outlined">group_add</span> Upload
</a>

<a class="navbar-item" href="/files/credentials" download="{{ session.authInfo.domain }}.users.csv">
<span class="material-symbols-outlined">save_as</span> Save Credentials
</a>
{% include "components/button_upload.html" className="navbar-item" %}
{% include "components/button_save_credentials.html" className="navbar-item" %}

<a class="navbar-item" href="/logout">
<span class="material-symbols-outlined">logout</span> Logout
Expand Down
3 changes: 3 additions & 0 deletions src/public/components/button_save_credentials.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<a class="{{ include.className | default: "button is-primary" }}" href="/files/credentials" download="{{ session.authInfo.domain }}.users.csv">
<span class="material-symbols-outlined">save_as</span> Save Credentials
</a>
3 changes: 3 additions & 0 deletions src/public/components/button_upload.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<a class="{{ include.className | default: "button is-primary" }}" hx-post="/app/apply-changes" hx-swap="none">
<span class="material-symbols-outlined">group_add</span> Upload
</a>
1 change: 0 additions & 1 deletion src/public/components/search_input.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
hx-target="#search_results_{{ property_name }}" hx-swap="innerHTML"
{% if include.required %} required {% endif %}
{% if include.data[property_name] %}value="{{ include.data[property_name] }}" {% endif %}
{% if oob %} hx-swap-oob="true" {%endif%}
hx-encoding="application/x-www-form-urlencoded"
/>
<div id="search_results_{{ property_name }}" class="control"></div>
Expand Down
31 changes: 31 additions & 0 deletions src/public/place/directive.html
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we have consistent header sizes and spacing with the views that already exist on the page? I also find the shadow/elevation on the boxes unnecessary

Copy link
Member Author

Choose a reason for hiding this comment

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

Can we have consistent header sizes and spacing with the views that already exist on the page?

Sorry, I don't know what this means. Would you clarify?

Copy link
Member Author

@kennsippell kennsippell Feb 12, 2024

Choose a reason for hiding this comment

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

I also find the shadow/elevation on the boxes unnecessary

This is what the UI looks like without boxes. I don't particularly like this because nothing is aligned. I find there is no line to the page.
image

I'm currently using the default box control from Bulma. Can I ask what you would like to see? Are you thinking no boxes (above) or less elevated boxes? Happy to implement, but I'm really bad at design and need a little more input to make your dream a reality.

Copy link
Collaborator

@freddieptf freddieptf Feb 12, 2024

Choose a reason for hiding this comment

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

I meant the headers in the directives are super huge compared to the other headers on the page. I find the elevation weird because it's makes the boxes a level higher than the navbar. Also the two boxes don't share the same background color. It's not a blocker so we can keep it and get a designer to have a look

Copy link
Member Author

Choose a reason for hiding this comment

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

Capturing in #67 to get a designer to look at this. For now I'll reduce header size, match background color, remove elevation, and send another screenshot.

Copy link
Member Author

Choose a reason for hiding this comment

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

image

Choose a reason for hiding this comment

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

it looks better the way it was before

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<div class="columns is-3 is-variable" id="directive" hx-swap-oob="true">
<div class="column has-background-light is-9">
<section class="hero is-small is-light">
<div class="hero-body">
{% if progress.inProgressCount > 0 %}
{% include "place/directive_3_in_progress.html" %}
{% elsif progress.stagedCount > 0 %}
{% include "place/directive_2_prompt_upload.html" %}
{% elsif progress.successCount > 0 %}
{% include "place/directive_4_prompt_save.html" %}
{% else %}
{% include "place/directive_1_get_started.html" %}
{% endif %}
</div>
</section>
</div>

<div class="column is-3 has-background-light">
<section class="hero is-small is-light">
<div class="hero-body">
<p class="title is-5">Summary</p>
<ul>
<li><span class="tag">{{ progress.stagedCount }} staged</span></li>
<li><span class="tag is-warning">{{ progress.validationErrorCount }} validation errors</span> </li>
<li><span class="tag is-success">{{ progress.completeCount }} uploaded</span> </li>
<li><span class="tag is-danger">{{ progress.failureCount }} failures</span> </li>
</ul>
</div>
</div>
</section>
</div>
22 changes: 22 additions & 0 deletions src/public/place/directive_1_get_started.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<p class="title is-5">
Getting Started...
</p>
<p>
To get started, add some users which you want to create on the <i>{{ session.authInfo.friendly }}</i> instance.
</p>

{% for contactType in contactTypes %}
<div class="navbar-menu">
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link is-arrowless button" href="/add-place?type={{ contactType.name }}&op=new">
<span class="material-symbols-outlined">add</span> Add {{contactType.friendly}}
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="/add-place?type={{ contactType.name }}&op=new">Create New</a>
<a class="navbar-item" href="/add-place?type={{ contactType.name }}&op=replace">Replace Existing</a>
<a class="navbar-item" href="/add-place?type={{ contactType.name }}&op=bulk">Upload from CSV</a>
<a class="navbar-item" href="/move/{{ contactType.name }}">Move</a>
</div>
</div>
</div>
{% endfor %}
15 changes: 15 additions & 0 deletions src/public/place/directive_2_prompt_upload.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<p class="title is-5">
Upload When Ready
</p>
<p>
You now have some <i>staged users</i> below which are ready for upload.
When everything looks right, upload them to <i>{{ session.authInfo.friendly }}</i>.
</p>

<div class="columns is-centered">
<div class="column is-4 has-text-centered">
<a class="button is-primary" data-target="progress-modal" hx-post="/app/apply-changes" hx-swap="none">
<span class="material-symbols-outlined">group_add</span> Upload
</a>
</div>
</div>
5 changes: 5 additions & 0 deletions src/public/place/directive_3_in_progress.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<p class="title is-5">
Upload In Progress...
</p>

<progress class="progress is-medium is-dark" value="{{ progress.completeCount }}" max="{{ progress.totalCount }}">{{ progress.percent }}</progress>
13 changes: 13 additions & 0 deletions src/public/place/directive_4_prompt_save.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<p class="title is-5">
Download Resulting User Credentials
</p>

<p>
Usernames and passwords are now available for download. Click the button below to download the credentials as a file.
</p>

<div class="columns is-centered">
<div class="column is-4 has-text-centered">
{% include "components/button_save_credentials.html" %}
</div>
</div>
Loading
Loading