Skip to content

Commit

Permalink
Merge branch 'main' into 862-reject_duplicate_submissions
Browse files Browse the repository at this point in the history
  • Loading branch information
noliveleger committed Oct 15, 2024
2 parents b227f5b + 7356674 commit 90b3f85
Show file tree
Hide file tree
Showing 59 changed files with 887 additions and 654 deletions.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ ENV KPI_LOGS_DIR=/srv/logs \
NGINX_STATIC_DIR=/srv/static \
KPI_SRC_DIR=/srv/src/kpi \
KPI_MEDIA_DIR=/srv/src/kpi/media \
OPENROSA_MEDIA_DIR=/srv/src/kobocat/media \
KPI_NODE_PATH=/srv/src/kpi/node_modules \
TMP_DIR=/srv/tmp \
UWSGI_USER=kobo \
Expand Down
17 changes: 13 additions & 4 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,19 @@ fi
echo 'Restore permissions on logs folder'
chown -R "${UWSGI_USER}:${UWSGI_GROUP}" "${KPI_LOGS_DIR}"

# This can take a while when starting a container with lots of media files.
# Maybe we should add a disclaimer as we do in KoBoCAT to let the users
# do it themselves
chown -R "${UWSGI_USER}:${UWSGI_GROUP}" "${KPI_MEDIA_DIR}"
# `chown -R` becomes very slow once a fair amount of media has been collected,
# so reset ownership of the media directory *only* (i.e., non-recursive)
echo 'Resetting ownership of media directories...'
chown "${UWSGI_USER}:${UWSGI_GROUP}" "${KPI_MEDIA_DIR}"
chown "${UWSGI_USER}:${UWSGI_GROUP}" "${OPENROSA_MEDIA_DIR}"
echo 'Done.'
echo '%%%%%%% NOTICE %%%%%%%'
echo '% To avoid long delays, we no longer reset ownership *recursively*'
echo '% every time this container starts. If you have trouble with'
echo '% permissions, please run the following command inside the KPI container:'
echo "% chown -R \"${UWSGI_USER}:${UWSGI_GROUP}\" \"${KPI_MEDIA_DIR}\""
echo "% chown -R \"${UWSGI_USER}:${UWSGI_GROUP}\" \"${OPENROSA_MEDIA_DIR}\""
echo '%%%%%%%%%%%%%%%%%%%%%%'

echo 'KPI initialization completed.'

Expand Down
12 changes: 0 additions & 12 deletions jsapp/js/account/accountSettings.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,6 @@
.form-modal__item--username {
min-height: 32px;
padding-left: sizes.$x50;

.account-box__initials {
display: inline-block;
vertical-align: middle;
margin-right: 15px;
}

h4 {
display: inline-block;
vertical-align: middle;
font-size: 16px;
}
}
}

Expand Down
13 changes: 3 additions & 10 deletions jsapp/js/account/accountSettingsRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {unstable_usePrompt as usePrompt} from 'react-router-dom';
import bem, {makeBem} from 'js/bem';
import sessionStore from 'js/stores/session';
import './accountSettings.scss';
import {notify, stringToColor} from 'js/utils';
import {notify} from 'js/utils';
import {dataInterface} from '../dataInterface';
import AccountFieldsEditor from './accountFieldsEditor.component';
import Icon from 'js/components/common/icon';
import Avatar from 'js/components/common/avatar';
import envStore from 'js/envStore';
import {
getInitialAccountFieldsValues,
Expand Down Expand Up @@ -134,9 +134,6 @@ const AccountSettings = observer(() => {
};

const accountName = sessionStore.currentAccount.username;
const initialsStyle = {
background: `#${stringToColor(accountName)}`,
};

return (
<bem.AccountSettings onSubmit={updateProfile}>
Expand All @@ -152,11 +149,7 @@ const AccountSettings = observer(() => {

<bem.AccountSettings__item m={'column'}>
<bem.AccountSettings__item m='username'>
<bem.AccountBox__initials style={initialsStyle}>
{accountName.charAt(0)}
</bem.AccountBox__initials>

<h4>{accountName}</h4>
<Avatar size='m' username={accountName} isUsernameVisible/>
</bem.AccountSettings__item>

{sessionStore.isInitialLoadComplete && form.isUserDataLoaded && (
Expand Down
8 changes: 2 additions & 6 deletions jsapp/js/account/changePasswordRoute.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import DocumentTitle from 'react-document-title';
import {observer} from 'mobx-react';
import sessionStore from 'js/stores/session';
import bem, {makeBem} from 'js/bem';
import {stringToColor} from 'js/utils';
import {withRouter} from 'js/router/legacy';
import type {WithRouterProps} from 'jsapp/js/router/legacy';
import './accountSettings.scss';
import styles from './changePasswordRoute.module.scss';
import UpdatePasswordForm from './security/password/updatePasswordForm.component';
import Button from 'js/components/common/button';
import Avatar from 'js/components/common/avatar';

bem.AccountSettings = makeBem(null, 'account-settings');
bem.AccountSettings__left = makeBem(bem.AccountSettings, 'left');
Expand All @@ -28,7 +28,6 @@ const ChangePasswordRoute = class ChangePassword extends React.Component<WithRou
}

const accountName = sessionStore.currentAccount.username;
const initialsStyle = {background: `#${stringToColor(accountName)}`};

return (
<DocumentTitle title={`${accountName} | KoboToolbox`}>
Expand All @@ -44,10 +43,7 @@ const ChangePasswordRoute = class ChangePassword extends React.Component<WithRou

<bem.AccountSettings__item m='column'>
<bem.AccountSettings__item m='username'>
<bem.AccountBox__initials style={initialsStyle}>
{accountName.charAt(0)}
</bem.AccountBox__initials>
<h4>{accountName}</h4>
<Avatar size='m' username={accountName} isUsernameVisible />
</bem.AccountSettings__item>

<div className={styles.fieldsWrapper}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
import React from 'react';

// Partial components
import Button from 'js/components/common/button';
// import Button from 'js/components/common/button';
import PaginatedQueryUniversalTable from 'js/universalTable/paginatedQueryUniversalTable.component';

// Utilities
import useAccessLogsQuery, {type AccessLog} from 'js/query/queries/accessLogs.query';
import {formatTime} from 'js/utils';
import sessionStore from 'js/stores/session';
// import sessionStore from 'js/stores/session';

// Styles
import securityStyles from 'js/account/security/securityRoute.module.scss';

export default function AccessLogsSection() {
function logOutAllSessions() {
sessionStore.logOutAll();
}
// function logOutAllSessions() {
// sessionStore.logOutAll();
// }

return (
<>
Expand All @@ -43,11 +43,21 @@ export default function AccessLogsSection() {
columns={[
// The `key`s of these columns are matching the `AccessLog` interface
// properties (from `accessLogs.query.ts` file) using dot notation.
{key: 'metadata.source', label: t('Source')},
{
key: 'metadata.source',
label: t('Source'),
cellFormatter: (log: AccessLog) => {
if (log.metadata.auth_type === 'submission-group') {
return t('Data Submissions (##count##)').replace('##count##', String(log.count));
} else {
return log.metadata.source;
}
},
},
{
key: 'date_created',
label: t('Last activity'),
cellFormatter: (date: string) => formatTime(date),
cellFormatter: (log: AccessLog) => formatTime(log.date_created),
},
{key: 'metadata.ip_address', label: t('IP Address')},
]}
Expand Down
4 changes: 4 additions & 0 deletions jsapp/js/bem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ interface bemInstances {

/**
* Container for holding all BEM definitions.
*
* @deprecated Use CSS Modules and regular HTML tags.
*/
const bem: bemInstances = {}

Expand Down Expand Up @@ -45,6 +47,8 @@ interface BemInstance extends React.ComponentClass<BemComponentProps, {}> {
* For first parameter pass `null` to create a Block component,
* or pass existing Block component to create Element component (a child).
* Please no angle brackets for `htmlTagName`.
*
* @deprecated Use CSS Modules and regular HTML tags.
*/
export function makeBem(
parent: BemInstance | null,
Expand Down
1 change: 0 additions & 1 deletion jsapp/js/bemComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ bem.LoginBox = makeBem(null, 'login-box');

bem.AccountBox = makeBem(null, 'account-box');
bem.AccountBox__name = makeBem(bem.AccountBox, 'name', 'div');
bem.AccountBox__initials = makeBem(bem.AccountBox, 'initials', 'span');
bem.AccountBox__menu = makeBem(bem.AccountBox, 'menu', 'ul');
bem.AccountBox__menuLI = makeBem(bem.AccountBox, 'menu-li', 'li');
bem.AccountBox__menuItem = makeBem(bem.AccountBox, 'menu-item', 'div');
Expand Down
6 changes: 5 additions & 1 deletion jsapp/js/components/assetsTable/assetsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,17 @@ interface AssetsTableState {
}

/**
* DEPRECATED-ish (see below)
* Displays a table of assets. This old-ish component is handling three routes:
* - My Library
* - Public Collections
* - Single Collection
* The new and shiny component that handles Projects List is `ProjectsTable`,
* and in the future it should become (if possible) the only one to be used.
*
* @deprecated There is no clear replacement as of yet. We have a better
* `ProjectsTable` component, but ultimately wa are aiming at using
* `react-table` more. See `UniversalTable` for the possible "final" way of
* handling tables.
*/
export default class AssetsTable extends React.Component<
AssetsTableProps,
Expand Down
40 changes: 30 additions & 10 deletions jsapp/js/components/common/avatar.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,45 @@
@use 'scss/colors';
@use 'scss/mixins';

.root {
$avatar-size-s: 24px;
$avatar-size-m: 32px;
$avatar-size-l: 48px;

@mixin avatar-size($size) {
// This is the gap between the initials and the (optional) username
gap: $size * 0.25;

.initials {
width: $size;
height: $size;
border-radius: $size;
line-height: $size;
font-size: $size * 0.6;
}
}

.avatar {
@include mixins.centerRowFlex;
}

.avatar-size-s {
@include avatar-size($avatar-size-s);
}

.avatar-size-m {
@include avatar-size($avatar-size-m);
}

.avatar-size-l {
@include avatar-size($avatar-size-l);
}

.initials {
width: sizes.$x20;
text-align: center;
text-transform: uppercase;
padding: 0 sizes.$x6;
border-radius: sizes.$x20;
display: inline-block;
vertical-align: middle;
line-height: sizes.$x20;
font-size: sizes.$x12;
color: colors.$kobo-white;
// actual background color is provided by JS, this is just safeguard
background-color: colors.$kobo-storm;

&:not(:last-child) {
margin-right: sizes.$x5;
}
}
60 changes: 60 additions & 0 deletions jsapp/js/components/common/avatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import type {ComponentStory, ComponentMeta} from '@storybook/react';

import Avatar from './avatar';
import type {AvatarSize} from './avatar';

const avatarSizes: AvatarSize[] = ['s', 'm', 'l'];

export default {
title: 'common/Avatar',
component: Avatar,
argTypes: {
username: {type: 'string'},
size: {
options: avatarSizes,
control: {type: 'select'},
},
isUsernameVisible: {type: 'boolean'},
},
} as ComponentMeta<typeof Avatar>;

const Template: ComponentStory<typeof Avatar> = (args) => <Avatar {...args} />;

export const Primary = Template.bind({});
Primary.args = {
username: 'Leszek',
size: avatarSizes[0],
isUsernameVisible: true,
};

// We want to test how the avatar colors look like with some ~random usernames.
const bulkUsernames = [
// NATO phonetic alphabet (https://en.wikipedia.org/wiki/NATO_phonetic_alphabet)
'Alfa', 'Bravo', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel',
'India', 'Juliett', 'Kilo', 'Lima', 'Mike', 'November', 'Oscar', 'Papa',
'Quebec', 'Romeo', 'Sierra', 'Tango', 'Uniform', 'Victor', 'Whiskey', 'Xray',
'Yankee', 'Zulu',
// Top 100 most popular names in the world (https://forebears.io/earth/forenames)
'Maria', 'Nushi', 'Mohammed', 'Jose', 'Muhammad', 'Mohamed', 'Wei', 'Mohammad',
'Ahmed', 'Yan', 'Ali', 'John', 'David', 'Li', 'Abdul', 'Ana', 'Ying', 'Michael',
'Juan', 'Anna', 'Mary', 'Jean', 'Robert', 'Daniel', 'Luis', 'Carlos', 'James',
'Antonio', 'Joseph', 'Hui', 'Elena', 'Francisco', 'Hong', 'Marie', 'Min', 'Lei',
'Yu', 'Ibrahim', 'Peter', 'Fatima', 'Aleksandr', 'Richard', 'Xin', 'Bin',
'Paul', 'Ping', 'Lin', 'Olga', 'Sri', 'Pedro', 'William', 'Rosa', 'Thomas',
'Jorge', 'Yong', 'Elizabeth', 'Sergey', 'Ram', 'Patricia', 'Hassan', 'Anita',
'Manuel', 'Victor', 'Sandra', 'Ming', 'Siti', 'Miguel', 'Emmanuel', 'Samuel',
'Ling', 'Charles', 'Sarah', 'Mario', 'Joao', 'Tatyana', 'Mark', 'Rita',
'Martin', 'Svetlana', 'Patrick', 'Natalya', 'Qing', 'Ahmad', 'Martha', 'Andrey',
'Sunita', 'Andrea', 'Christine', 'Irina', 'Laura', 'Linda', 'Marina', 'Carmen',
'Ghulam', 'Vladimir', 'Barbara', 'Angela', 'George', 'Roberto', 'Peng',
];
export const BulkTest = () => (
<div style={{display: 'flex', flexWrap: 'wrap', gap: '10px'}}>
{bulkUsernames.map((username) => (
<div key={username}>
<Avatar size='m' username={username} isUsernameVisible/>
</div>
))}
</div>
);
37 changes: 33 additions & 4 deletions jsapp/js/components/common/avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,48 @@
import {stringToColor} from 'jsapp/js/utils';
import React from 'react';
import cx from 'classnames';
import styles from './avatar.module.scss';

export type AvatarSize = 'l' | 'm' | 's';

/**
* A simple function that generates hsl color from given string. Saturation and
* lightness is not random, just the hue.
*/
function stringToHSL(string: string, saturation: number, lightness: number) {
let hash = 0;
for (let i = 0; i < string.length; i++) {
hash = string.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash;
}
return `hsl(${(hash % 360)}, ${saturation}%, ${lightness}%)`;
}

interface AvatarProps {
/**
* First letter of the username would be used as avatar. Whole username would
* be used to generate the color of the avatar.
*/
username: string;
/**
* Username is not being displayed by default.
*/
isUsernameVisible?: boolean;
size: AvatarSize;
}

export default function Avatar(props: AvatarProps) {
return (
<div className={styles.root}>
<div className={styles.initials} style={{background: `#${stringToColor(props.username)}`}}>
<div className={cx(styles.avatar, styles[`avatar-size-${props.size}`])}>
<div
className={styles.initials}
style={{backgroundColor: `${stringToHSL(props.username, 80, 40)}`}}
>
{props.username.charAt(0)}
</div>

<label>{props.username}</label>
{props.isUsernameVisible &&
<label>{props.username}</label>
}
</div>
);
}
Loading

0 comments on commit 90b3f85

Please sign in to comment.