Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
garronej committed Jul 19, 2023
1 parent 2ad286d commit c012340
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 17 deletions.
42 changes: 41 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,52 @@ REACT_APP_HEADER_HIDE_ONYXIA=false

REACT_APP_DISABLE_HOME_PAGE=false

# Enable to share a communication message to all users, it will be displayed in a banner on the top of the page.
# https://github.com/InseeFrLab/onyxia-web/assets/6702424/5c3345a6-b3e1-4620-af21-d8a4dad72af9
#
# Examples:
#
# GLOBAL_ALERT: "See what's **new**! [Blog post](https://example.com)!"
#
# You can also provide a severity among info, success, warning, error
#
# GLOBAL_ALERT: |
# {
# "severity": "success",
# "message": "See what's **new**! [Blog post](https://example.com)!"
# }
#
# You can localize the message by providing a map language iso2 -> message
#
# GLOBAL_ALERT: |
# {
# "en": "See what's **new**! [Blog post](https://example.com)!",
# "fr": "Voyez ce qui est **nouveau**! [Article de blog](https://example.fr)!"
# }
#
# It works the same when you have a severity
#
# GLOBAL_ALERT: |
# {
# "severity": "success",
# "message": {
# "en": "See what's **new**! [Blog post](https://example.com)!",
# "fr": "Voyez ce qui est **nouveau**! [Article de blog](https://example.fr)!"
# }
# }
#
# Note regarding accessibility, when the message isn't provided under the form of a map language iso2 -> message
# We will assume the message is in english and screen reader will read it with the english synthetizer.
# Even if 100% of your users as french (for example) it's better to provide the message as a map { "fr": "Le message" }
# rather that just "Le message".
#
REACT_APP_GLOBAL_ALERT=

# To add external links in the left bar.
# In Values.yaml you can go multi line by using '|' but in .env file it must be on a sigle line. (see .env.local.sample)
# 'icon' must be a valid icon identifier from: https://github.com/InseeFrLab/onyxia-web/blob/main/src/ui/theme.tsx#L115-L159
# 'label' can either be a string or a map language iso2 -> text. Not every supported language have to include a translation.
# SIDEBAR_LINKS = |
# EXTRA_LEFTBAR_ITEMS: |
# [
# {
# "label": "Legacy",
Expand Down
1 change: 1 addition & 0 deletions .env.local.sample-insee
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ REACT_APP_HEADER_USECASE_DESCRIPTION=SSP Cloud
REACT_APP_TERMS_OF_SERVICES={ "en": "https://www.sspcloud.fr/tos_en.md", "fr": "https://www.sspcloud.fr/tos_fr.md" }
REACT_APP_HEADER_LINKS=[{ "label": { "en": "Training", "fr": "Formations", "zh-CN":"教程", "it": "Formazioni" }, "iconId": "training", "url": "https://www.sspcloud.fr/formation" }, { "label": { "en": "Documentation", "fr": "Documentation", "zh-CN":"文档", "it": "Documentazione" }, "iconId": "language", "url": "https://docs.sspcloud.fr/" }]
REACT_APP_DESCRIPTION=Plateforme mutualisée de services de traitement des données statistiques et de datascience!
#REACT_APP_GLOBAL_ALERT={ "severity": "success", "message": { "en": "See what's **new**! [Blog post](https://example.com)!", "fr": "Découvrez les nouveautés ! [Article de blog](https://example.com)!" } }
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"file-saver": "^2.0.2",
"filereader-stream": "^2.0.0",
"https-browserify": "^1.0.0",
"i18nifty": "^1.5.10",
"i18nifty": "^1.6.1",
"jwt-decode": "^3.1.2",
"jwt-simple": "^0.5.6",
"keycloak-js": "21.0.2",
Expand All @@ -64,7 +64,7 @@
"minio": "^7.1.1",
"moment": "^2.29.1",
"mustache": "^4.2.0",
"onyxia-ui": "^0.56.0",
"onyxia-ui": "^0.57.1",
"path": "^0.12.7",
"path-browserify": "^1.0.1",
"powerhooks": "^0.26.14",
Expand Down Expand Up @@ -114,7 +114,8 @@
"no-labels": "off",
"prefer-const": "off",
"no-sequences": "off",
"tss-unused-classes/unused-classes": "warn"
"tss-unused-classes/unused-classes": "warn",
"no-lone-blocks": "off"
}
},
"husky": {
Expand Down
99 changes: 97 additions & 2 deletions src/ui/App/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "minimal-polyfills/Object.fromEntries";
import { useMemo, useEffect, Suspense } from "react";
import { useMemo, useEffect, useReducer, Suspense } from "react";
import { Header } from "ui/shared/Header";
import { LeftBar, makeStyles, type IconId } from "ui/theme";
import type { LeftBarProps } from "onyxia-ui/LeftBar";
Expand Down Expand Up @@ -30,6 +30,11 @@ import { pages } from "ui/pages";
import { assert, type Equals } from "tsafe/assert";
import { useIsI18nFetching } from "ui/i18n";
import { useLang } from "ui/i18n";
import { Alert } from "onyxia-ui/Alert";
import { simpleHash } from "ui/tools/simpleHash";
import { Markdown } from "onyxia-ui/Markdown";
import { type LocalizedString } from "ui/i18n";
import { getGlobalAlert } from "ui/env";

const { CoreProvider } = createCoreProvider({
"apiUrl": getEnv().ONYXIA_API_URL,
Expand Down Expand Up @@ -189,6 +194,21 @@ function ContextualizedApp() {

return (
<div ref={rootRef} className={classes.root}>
{(() => {
const globalAlert = getGlobalAlert();

if (globalAlert === undefined) {
return null;
}

return (
<GlobalAlert
className={classes.globalAlert}
severity={globalAlert.severity}
message={globalAlert.message}
/>
);
})()}
{(() => {
const common = {
"className": classes.header,
Expand Down Expand Up @@ -321,15 +341,22 @@ export const { i18n } = declareComponentKeys<
const useStyles = makeStyles({ "name": { App } })(theme => {
const footerHeight = 32;

const rootRightLeftMargin = theme.spacing(4);

return {
"root": {
"height": "100%",
"display": "flex",
"flexDirection": "column",
"backgroundColor": theme.colors.useCases.surfaces.background,
"margin": theme.spacing({ "topBottom": 0, "rightLeft": 4 }),
"margin": `0 ${rootRightLeftMargin}px`,
"position": "relative"
},
"globalAlert": {
"position": "relative",
"width": `calc(100% + 2 * ${rootRightLeftMargin}px)`,
"left": -rootRightLeftMargin
},
"header": {
"paddingBottom": 0 //For the LeftBar shadow
},
Expand Down Expand Up @@ -370,6 +397,74 @@ const useStyles = makeStyles({ "name": { App } })(theme => {
};
});

const { GlobalAlert } = (() => {
type GlobalAlertProps = {
className?: string;
// Default value is "info"
severity: "success" | "info" | "warning" | "error" | undefined;
message: LocalizedString;
};

const localStorageKeyPrefix = "global-alert-";

function GlobalAlert(props: GlobalAlertProps) {
const { className, severity = "info", message } = props;

const { resolveLocalizedStringDetailed } = useResolveLocalizedString({
"labelWhenMismatchingLanguage": true
});

const localStorageKey = useMemo(
() => `${localStorageKeyPrefix}${simpleHash(severity + message)}-closed`,
[severity, message]
);

const [trigger, pullTrigger] = useReducer(() => ({}), {});

const isClosed = useMemo(() => {
// Remove all the local storage keys that are not used anymore.
for (const key of Object.keys(localStorage)) {
if (!key.startsWith(localStorageKeyPrefix) || key === localStorageKey) {
continue;
}
localStorage.removeItem(key);
}

const value = localStorage.getItem(localStorageKey);

return value === "true";
}, [localStorageKey, trigger]);

return (
<Alert
className={className}
severity={severity}
doDisplayCross
isClosed={isClosed}
onClose={() => {
localStorage.setItem(localStorageKey, "true");
pullTrigger();
}}
>
{(() => {
const { str, langAttrValue } =
resolveLocalizedStringDetailed(message);

const markdownNode = <Markdown>{str}</Markdown>;

return langAttrValue === undefined ? (
markdownNode
) : (
<div lang={langAttrValue}>{markdownNode}</div>
);
})()}
</Alert>
);
}

return { GlobalAlert };
})();

/**
* This hook to two things:
* - It sets whether or not the dark mode is enabled based on
Expand Down
37 changes: 36 additions & 1 deletion src/ui/env.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import "minimal-polyfills/Object.fromEntries";
import type { LocalizedString } from "ui/i18n";
import { type LocalizedString, zLocalizedString } from "ui/i18n";
import { getEnv } from "env";
import { symToStr } from "tsafe/symToStr";
import memoize from "memoizee";
import { z } from "zod";
import { assert } from "tsafe/assert";

export type AdminProvidedLink = {
Expand Down Expand Up @@ -82,3 +83,37 @@ export const getDoHideOnyxia = memoize((): boolean => {

return HEADER_HIDE_ONYXIA === "true";
});

export const getGlobalAlert = memoize(() => {
const key = "GLOBAL_ALERT";

const envValue = getEnv()[key];

if (envValue === "") {
return undefined;
}

if (/^\s*\{.*\}\s*$/.test(envValue)) {
const zSchema = z.object({
"severity": z.enum(["error", "warning", "info", "success"]),
"message": zLocalizedString
});

let parsedEnvValue: z.infer<typeof zSchema>;

try {
parsedEnvValue = JSON.parse(envValue);

zSchema.parse(parsedEnvValue);
} catch {
throw new Error(`${key} is malformed, ${envValue}`);
}

return parsedEnvValue;
}

return {
"severity": "info" as const,
"message": envValue
};
});
30 changes: 29 additions & 1 deletion src/ui/i18n/i18n.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { createI18nApi, declareComponentKeys } from "i18nifty";
import { languages, fallbackLanguage } from "./types";
import { languages, fallbackLanguage, Language } from "./types";
import { ComponentKey } from "./types";
import { assert, type Equals } from "tsafe/assert";
import { statefulObservableToStatefulEvt } from "powerhooks/tools/StatefulObservable/statefulObservableToStatefulEvt";
import { z } from "zod";
export { declareComponentKeys };

export type LocalizedString = Parameters<typeof resolveLocalizedString>[0];
Expand Down Expand Up @@ -32,3 +34,29 @@ export const {
export const evtLang = statefulObservableToStatefulEvt({
"statefulObservable": $lang
});

export const zLanguage = z.union([
z.literal("en"),
z.literal("fr"),
z.literal("zh-CN"),
z.literal("no"),
z.literal("fi"),
z.literal("nl"),
z.literal("it")
]);

{
type Got = ReturnType<(typeof zLanguage)["parse"]>;
type Expected = Language;

assert<Equals<Got, Expected>>();
}

export const zLocalizedString = z.union([z.string(), z.record(zLanguage, z.string())]);

{
type Got = ReturnType<(typeof zLocalizedString)["parse"]>;
type Expected = LocalizedString;

assert<Equals<Got, Expected>>();
}
9 changes: 9 additions & 0 deletions src/ui/tools/simpleHash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function simpleHash(str: string) {
let hash = 2166136261; // FNV offset basis
for (let i = 0; i < str.length; i++) {
hash ^= str.charCodeAt(i);
hash *= 16777619; // FNV prime
}
// Use bitwise XOR for converting hash to 32-bit integer, then convert to string
return (hash >>> 0).toString();
}
28 changes: 19 additions & 9 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9162,12 +9162,12 @@ husky@^4.3.8:
slash "^3.0.0"
which-pm-runs "^1.0.0"

i18nifty@^1.5.10:
version "1.5.10"
resolved "https://registry.yarnpkg.com/i18nifty/-/i18nifty-1.5.10.tgz#359b10965bbacb20fc1090df467bd3b4dd5c913e"
integrity sha512-jjKOG/tA3Ih1DRGN6ZQO2TKOg2eyk426mX+NSlVdwmI4ghhcAD/Dwz7uS9PZ7lsRomYh5W1oVQbMsBrL8TkmKg==
i18nifty@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/i18nifty/-/i18nifty-1.6.1.tgz#5c30ea51a84e816a7f3c1c298f08bb9e5feb9c24"
integrity sha512-J4EgG6CNMtLbOqMtHBa82ZaxvQLNUBHXlmjtYtW29y49J6E59BpsvKtxoiz4xiefk6dK6YoT2oSjh0TX5Vyt3Q==
dependencies:
powerhooks "^0.26.14"
powerhooks "^0.27.0"
tsafe "^1.6.4"

[email protected]:
Expand Down Expand Up @@ -11858,10 +11858,10 @@ onetime@^5.1.0, onetime@^5.1.2:
dependencies:
mimic-fn "^2.1.0"

onyxia-ui@^0.56.0:
version "0.56.0"
resolved "https://registry.yarnpkg.com/onyxia-ui/-/onyxia-ui-0.56.0.tgz#9ed10f12210cda995c4d7292e1bd111be56ff53f"
integrity sha512-CGXcR26wQhkba4tBaIv7+GqTFrzDkv3rgaL+u4btKL5hUIAxTWhVLcJUHLAMOcxG2+2wPU1xC/daYlWlUTcpnA==
onyxia-ui@^0.57.1:
version "0.57.1"
resolved "https://registry.yarnpkg.com/onyxia-ui/-/onyxia-ui-0.57.1.tgz#3603769e462ed8d6f1201777baa52e45554e6f0b"
integrity sha512-wwAKE/2qJCR8C1tc3tiF4PbOj0LKB4J4mQBV7A8UAyKbyju1RpNCR+AzbOD57rxRsn76pZqBGRYzrVI8hRLxGA==
dependencies:
"@mui/icons-material" "^5.11.16"
color "3.1.3"
Expand Down Expand Up @@ -12989,6 +12989,16 @@ powerhooks@^0.26.14:
resize-observer-polyfill "^1.5.1"
tsafe "^1.6.4"

powerhooks@^0.27.0:
version "0.27.0"
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.27.0.tgz#600e750fae6c42682639634e80e1b12f59cfea4d"
integrity sha512-oA5t6jFI5wqxs5PSJrp5Q3DK51Z8k4/htlD6Hdpqzstth25wT44MOuBlEJITZnNMpOVWLpEzQnakSC4vV0Eaew==
dependencies:
evt "^2.4.22"
memoizee "^0.4.15"
resize-observer-polyfill "^1.5.1"
tsafe "^1.6.4"

prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
Expand Down

0 comments on commit c012340

Please sign in to comment.