From f16f927287fe0552f6ac22a1982589f4c3ce3268 Mon Sep 17 00:00:00 2001 From: Jeeva Kandasamy Date: Sat, 19 Sep 2026 08:09:46 +0530 Subject: [PATCH] use PatternFly light, dark, and system themes Remove custom data-repository GUI themes. Persist the choice in localStorage (mc-theme). Header has a compact joined theme control and a locale dropdown that shows the flag plus EN-UK. Dark mode uses PatternFly tokens for labels, forms, and widgets. Dashboard tiles no longer use a grey fill. --- CHANGELOG.md | 1 + docs/locales.md | 2 +- web-console/public/locales/de_DE.yaml | 4 + web-console/public/locales/en_GB.yaml | 4 + web-console/public/locales/en_US.yaml | 4 + web-console/public/locales/es_ES.yaml | 4 + web-console/public/locales/fr_FR.yaml | 4 + web-console/public/locales/he_IL.yaml | 4 + web-console/public/locales/hi_IN.yaml | 4 + web-console/public/locales/it_IT.yaml | 4 + web-console/public/locales/kn_IN.yaml | 4 + web-console/public/locales/ml_IN.yaml | 4 + web-console/public/locales/nl_NL.yaml | 4 + web-console/public/locales/pl_PL.yaml | 4 + web-console/public/locales/pt_PT.yaml | 4 + web-console/public/locales/ro_RO.yaml | 4 + web-console/public/locales/ru_RU.yaml | 4 + web-console/public/locales/ta_IN.yaml | 4 + web-console/public/locales/te_IN.yaml | 4 + web-console/public/locales/zh_CN.yaml | 4 + web-console/public/locales/zh_TW.yaml | 4 + web-console/src/App.js | 2 + .../src/Components/BasePage/ListBase.scss | 4 + .../src/Components/DataDisplay/Label.scss | 16 +- .../src/Components/DataDisplay/Resource.scss | 2 +- web-console/src/Components/Form/Form.scss | 4 +- web-console/src/Components/Graphs/Graphs.scss | 2 +- .../src/Components/Selector/Selector.scss | 4 +- .../Widgets/ControlPanel/Common/Common.scss | 10 +- .../Widgets/ImagePanel/ImagePanel.scss | 4 +- .../src/Components/Widgets/Widget.scss | 5 +- web-console/src/Constants/Common.js | 29 +++- web-console/src/Layout/Layout.jsx | 163 +++++++----------- web-console/src/Layout/Layout.scss | 139 ++++++++++++++- .../src/Pages/Dashboard/Dashboard.scss | 11 +- web-console/src/Pages/Topology/Topology.scss | 10 +- web-console/src/Theme/Theme.jsx | 82 ++++----- web-console/src/Theme/themeStorage.js | 29 ++++ web-console/src/i18n/languages.js | 9 + web-console/src/index.js | 3 + web-console/src/store/entities/theme.js | 7 +- web-console/src/theme.scss | 64 +++++-- 42 files changed, 485 insertions(+), 193 deletions(-) create mode 100644 web-console/src/Theme/themeStorage.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 331a7b30..efd60818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Draft for **2.3.0** (`versions.txt`). Compared to [v2.2.0](https://github.com/my ### Changed +- Console colour scheme is PatternFly **light**, **dark**, or **system** (follows the OS). Custom themes from the data repository (`labels.gui_theme`) are removed. The choice is stored in the browser (`localStorage` `mc-theme`). The header has a compact joined theme control and a locale dropdown (`🇬🇧 EN-UK`). Dark mode uses PatternFly tokens for labels, forms, topology, and widgets. Dashboard tiles no longer use a grey fill. - Release workflow builds host binaries (Go 1.27.1 in Actions) and copies only those binaries into Alpine 3.24 images on the same runner (no artifact upload/download). Images are pushed to GHCR, Quay, and Docker Hub as a single multi-arch `:${VERSION}` tag (no per-arch tags). `main` republishes the `development` pre-release by deleting and recreating it. - Policy **Import** uses `storage.Upsert` so `createdOn` / `modifiedOn` come from the backup file (same as other resources). Built-in policies are still skipped. - Built-in policies (admin / readwrite / readonly) are no longer rewritten on every start. `ModifiedOn` stays unless the code definition changed. Existing rows without `CreatedOn` are backfilled from `ModifiedOn` once. diff --git a/docs/locales.md b/docs/locales.md index 04e62881..babcb771 100644 --- a/docs/locales.md +++ b/docs/locales.md @@ -16,7 +16,7 @@ This document is the standing guide for **web-console translations**: how they l The UI uses `i18next` + `react-i18next` + `i18next-http-backend`. YAML is parsed with `js-yaml`. Detection order is Redux stored language, then the browser; unknown codes fall back to `en_GB`. Hyphens are normalized to underscores (`en-GB` → `en_GB`). -Every registered language **must** have a YAML file with the **same keys** as `en_GB.yaml`. As of this writing that is **978 keys** (flat count, including nested `dialog.*`, `helper_text.*`, `error.*`, `opts.*`). +Every registered language **must** have a YAML file with the **same keys** as `en_GB.yaml`. As of this writing that is **982 keys** (flat count, including nested `dialog.*`, `helper_text.*`, `error.*`, `opts.*`). ### Current languages diff --git a/web-console/public/locales/de_DE.yaml b/web-console/public/locales/de_DE.yaml index 79cecdb1..c51c9c4a 100644 --- a/web-console/public/locales/de_DE.yaml +++ b/web-console/public/locales/de_DE.yaml @@ -941,7 +941,11 @@ telegram: Telegram temperature: Temperatur text: Text the_open_source_controller: The Open Source Controller +theme: Design theme_color: Themenfarbe +theme_dark: Dunkel +theme_light: Hell +theme_system: System thickness: Dicke threshold_mode: Schwellenmodus thresholds: Schwellenwerte diff --git a/web-console/public/locales/en_GB.yaml b/web-console/public/locales/en_GB.yaml index b4da6704..38767cda 100644 --- a/web-console/public/locales/en_GB.yaml +++ b/web-console/public/locales/en_GB.yaml @@ -937,7 +937,11 @@ telegram: Telegram temperature: Temperature text: Text the_open_source_controller: The Open Source Controller +theme: Theme theme_color: Theme Colour +theme_dark: Dark +theme_light: Light +theme_system: System thickness: Thickness threshold_mode: Threshold Mode thresholds: Thresholds diff --git a/web-console/public/locales/en_US.yaml b/web-console/public/locales/en_US.yaml index 6bec5626..fdfafd04 100644 --- a/web-console/public/locales/en_US.yaml +++ b/web-console/public/locales/en_US.yaml @@ -937,7 +937,11 @@ telegram: Telegram temperature: Temperature text: Text the_open_source_controller: The Open Source Controller +theme: Theme theme_color: Theme Color +theme_dark: Dark +theme_light: Light +theme_system: System thickness: Thickness threshold_mode: Threshold Mode thresholds: Thresholds diff --git a/web-console/public/locales/es_ES.yaml b/web-console/public/locales/es_ES.yaml index 1fd5442c..214707e2 100644 --- a/web-console/public/locales/es_ES.yaml +++ b/web-console/public/locales/es_ES.yaml @@ -937,7 +937,11 @@ telegram: Telegram temperature: Temperatura text: Texto the_open_source_controller: The Open Source Controller +theme: Tema theme_color: Color del tema +theme_dark: Oscuro +theme_light: Claro +theme_system: Sistema thickness: Grosor threshold_mode: Modo de umbral thresholds: Umbrales diff --git a/web-console/public/locales/fr_FR.yaml b/web-console/public/locales/fr_FR.yaml index ed003215..8c926653 100644 --- a/web-console/public/locales/fr_FR.yaml +++ b/web-console/public/locales/fr_FR.yaml @@ -937,7 +937,11 @@ telegram: Telegram temperature: Température text: Texte the_open_source_controller: The Open Source Controller +theme: Thème theme_color: Couleur du thème +theme_dark: Sombre +theme_light: Clair +theme_system: Système thickness: Épaisseur threshold_mode: Mode de seuil thresholds: Seuils diff --git a/web-console/public/locales/he_IL.yaml b/web-console/public/locales/he_IL.yaml index 0ec22cfe..a30efe33 100644 --- a/web-console/public/locales/he_IL.yaml +++ b/web-console/public/locales/he_IL.yaml @@ -941,7 +941,11 @@ telegram: Telegram temperature: טמפרטורה text: טקסט the_open_source_controller: The Open Source Controller +theme: ערכת נושא theme_color: צבע ערכת נושא +theme_dark: כהה +theme_light: בהיר +theme_system: מערכת thickness: עובי threshold_mode: מצב סף thresholds: ספים diff --git a/web-console/public/locales/hi_IN.yaml b/web-console/public/locales/hi_IN.yaml index 9cd19ea9..e2577d28 100644 --- a/web-console/public/locales/hi_IN.yaml +++ b/web-console/public/locales/hi_IN.yaml @@ -937,7 +937,11 @@ telegram: Telegram temperature: तापमान text: पाठ the_open_source_controller: The Open Source Controller +theme: थीम theme_color: Theme रंग +theme_dark: गहरा +theme_light: हल्का +theme_system: सिस्टम thickness: मोटाई threshold_mode: Threshold मोड thresholds: Threshold diff --git a/web-console/public/locales/it_IT.yaml b/web-console/public/locales/it_IT.yaml index c984557f..2e5852c2 100644 --- a/web-console/public/locales/it_IT.yaml +++ b/web-console/public/locales/it_IT.yaml @@ -937,7 +937,11 @@ telegram: Telegram temperature: Temperatura text: Testo the_open_source_controller: The Open Source Controller +theme: Tema theme_color: Colore del tema +theme_dark: Scuro +theme_light: Chiaro +theme_system: Sistema thickness: Spessore threshold_mode: Modalità soglia thresholds: Soglie diff --git a/web-console/public/locales/kn_IN.yaml b/web-console/public/locales/kn_IN.yaml index c1c17a8d..8dce5322 100644 --- a/web-console/public/locales/kn_IN.yaml +++ b/web-console/public/locales/kn_IN.yaml @@ -937,7 +937,11 @@ telegram: Telegram temperature: ತಾಪಮಾನ text: ಪಠ್ಯ the_open_source_controller: The Open Source Controller +theme: ಥೀಮ್ theme_color: Theme ಬಣ್ಣ +theme_dark: ಗಾಢ +theme_light: ತಿಳಿ +theme_system: ಸಿಸ್ಟಂ thickness: ದಪ್ಪ threshold_mode: Threshold ವಿಧಾನ thresholds: Thresholdಗಳು diff --git a/web-console/public/locales/ml_IN.yaml b/web-console/public/locales/ml_IN.yaml index f2322b6d..88e4a730 100644 --- a/web-console/public/locales/ml_IN.yaml +++ b/web-console/public/locales/ml_IN.yaml @@ -937,7 +937,11 @@ telegram: Telegram temperature: താപനില text: വാചകം the_open_source_controller: The Open Source Controller +theme: തീം theme_color: Theme നിറം +theme_dark: ഇരുണ്ടത് +theme_light: ഇളം +theme_system: സിസ്റ്റം thickness: കനം threshold_mode: Threshold രീതി thresholds: Thresholdകൾ diff --git a/web-console/public/locales/nl_NL.yaml b/web-console/public/locales/nl_NL.yaml index 70384e2a..ba33f8ac 100644 --- a/web-console/public/locales/nl_NL.yaml +++ b/web-console/public/locales/nl_NL.yaml @@ -941,7 +941,11 @@ telegram: Telegram temperature: Temperatuur text: Tekst the_open_source_controller: The Open Source Controller +theme: Thema theme_color: Themakleur +theme_dark: Donker +theme_light: Licht +theme_system: Systeem thickness: Dikte threshold_mode: Drempelmodus thresholds: Drempels diff --git a/web-console/public/locales/pl_PL.yaml b/web-console/public/locales/pl_PL.yaml index 0638b949..ee3bbd5b 100644 --- a/web-console/public/locales/pl_PL.yaml +++ b/web-console/public/locales/pl_PL.yaml @@ -941,7 +941,11 @@ telegram: Telegram temperature: Temperatura text: Tekst the_open_source_controller: The Open Source Controller +theme: Motyw theme_color: Kolor motywu +theme_dark: Ciemny +theme_light: Jasny +theme_system: System thickness: Grubość threshold_mode: Tryb progu thresholds: Progi diff --git a/web-console/public/locales/pt_PT.yaml b/web-console/public/locales/pt_PT.yaml index a04d3ad1..e87fe177 100644 --- a/web-console/public/locales/pt_PT.yaml +++ b/web-console/public/locales/pt_PT.yaml @@ -937,7 +937,11 @@ telegram: Telegram temperature: Temperatura text: Texto the_open_source_controller: The Open Source Controller +theme: Tema theme_color: Cor do tema +theme_dark: Escuro +theme_light: Claro +theme_system: Sistema thickness: Espessura threshold_mode: Modo de limiar thresholds: Limiares diff --git a/web-console/public/locales/ro_RO.yaml b/web-console/public/locales/ro_RO.yaml index 4d955736..e39d1f73 100644 --- a/web-console/public/locales/ro_RO.yaml +++ b/web-console/public/locales/ro_RO.yaml @@ -937,7 +937,11 @@ telegram: Telegram temperature: Temperatură text: Text the_open_source_controller: The Open Source Controller +theme: Temă theme_color: Culoare temă +theme_dark: Întunecat +theme_light: Deschis +theme_system: Sistem thickness: Grosime threshold_mode: Mod prag thresholds: Praguri diff --git a/web-console/public/locales/ru_RU.yaml b/web-console/public/locales/ru_RU.yaml index 6acf23e0..8a5905aa 100644 --- a/web-console/public/locales/ru_RU.yaml +++ b/web-console/public/locales/ru_RU.yaml @@ -941,7 +941,11 @@ telegram: Telegram temperature: Температура text: Текст the_open_source_controller: The Open Source Controller +theme: Тема theme_color: Цвет темы +theme_dark: Тёмная +theme_light: Светлая +theme_system: Система thickness: Толщина threshold_mode: Режим порога thresholds: Пороги diff --git a/web-console/public/locales/ta_IN.yaml b/web-console/public/locales/ta_IN.yaml index 4bf7ee33..bfaad06a 100644 --- a/web-console/public/locales/ta_IN.yaml +++ b/web-console/public/locales/ta_IN.yaml @@ -941,7 +941,11 @@ telegram: Telegram temperature: வெப்பநிலை text: உரை the_open_source_controller: The Open Source Controller +theme: தீம் theme_color: Theme நிறம் +theme_dark: இருள் +theme_light: வெளிர் +theme_system: சிஸ்டம் thickness: தடிமன் threshold_mode: Threshold முறை thresholds: Threshold-கள் diff --git a/web-console/public/locales/te_IN.yaml b/web-console/public/locales/te_IN.yaml index 1aca6779..c8e8a227 100644 --- a/web-console/public/locales/te_IN.yaml +++ b/web-console/public/locales/te_IN.yaml @@ -937,7 +937,11 @@ telegram: Telegram temperature: ఉష్ణోగ్రత text: వచనం the_open_source_controller: The Open Source Controller +theme: థీమ్ theme_color: Theme రంగు +theme_dark: డార్క్ +theme_light: లైట్ +theme_system: సిస్టం thickness: మందం threshold_mode: Threshold విధానం thresholds: Thresholdలు diff --git a/web-console/public/locales/zh_CN.yaml b/web-console/public/locales/zh_CN.yaml index a1d27214..2d5bf101 100644 --- a/web-console/public/locales/zh_CN.yaml +++ b/web-console/public/locales/zh_CN.yaml @@ -941,7 +941,11 @@ telegram: Telegram temperature: 温度 text: 文本 the_open_source_controller: The Open Source Controller +theme: 主题 theme_color: 主题颜色 +theme_dark: 深色 +theme_light: 浅色 +theme_system: 系统 thickness: 厚度 threshold_mode: 阈值模式 thresholds: 阈值 diff --git a/web-console/public/locales/zh_TW.yaml b/web-console/public/locales/zh_TW.yaml index b889dc34..dfa96e14 100644 --- a/web-console/public/locales/zh_TW.yaml +++ b/web-console/public/locales/zh_TW.yaml @@ -941,7 +941,11 @@ telegram: Telegram temperature: 溫度 text: 文字 the_open_source_controller: The Open Source Controller +theme: 主題 theme_color: 主題顏色 +theme_dark: 深色 +theme_light: 淺色 +theme_system: 系統 thickness: 厚度 threshold_mode: 臨界值模式 thresholds: 臨界值 diff --git a/web-console/src/App.js b/web-console/src/App.js index 7c4074cb..76334d43 100644 --- a/web-console/src/App.js +++ b/web-console/src/App.js @@ -3,11 +3,13 @@ import { Provider } from "react-redux" import { PersistGate } from "redux-persist/integration/react" import IndexPage from "./Layout/IndexPage" import { persistor, store } from "./store/persister" +import Theme from "./Theme/Theme" function App() { return ( + Loading...}> diff --git a/web-console/src/Components/BasePage/ListBase.scss b/web-console/src/Components/BasePage/ListBase.scss index 4377653b..6422f45e 100644 --- a/web-console/src/Components/BasePage/ListBase.scss +++ b/web-console/src/Components/BasePage/ListBase.scss @@ -2,6 +2,10 @@ background-color: var(--pf-global--palette--blue-50); } +:root:where(.pf-theme-dark) .table-row-selected { + background-color: var(--pf-global--BackgroundColor--300); +} + .mc-table [type="checkbox"] { cursor: pointer; } diff --git a/web-console/src/Components/DataDisplay/Label.scss b/web-console/src/Components/DataDisplay/Label.scss index 17e6282f..6a7f013d 100644 --- a/web-console/src/Components/DataDisplay/Label.scss +++ b/web-console/src/Components/DataDisplay/Label.scss @@ -5,7 +5,7 @@ /* background-color: #f2f7ff; */ .key { display: inline-block; - color: #0303038c; + color: var(--pf-global--Color--200); min-width: 150px; padding-right: 5px; } @@ -42,6 +42,7 @@ --pf-c-label--BorderRadius: 0 2px 2px 0; --pf-c-label__content--before--BorderColor: #bee1f4; --pf-c-label--BackgroundColor: #bee1f4; + --pf-c-label__content--Color: #00659c; color: #00659c; padding-top: 4px; padding-bottom: 4px; @@ -50,12 +51,21 @@ .label-value { --pf-c-label--BorderRadius: 0 2px 2px 0; - --pf-c-label__content--before--BorderColor: #7dc3e8; - --pf-c-label--BackgroundColor: #7dc3e8; + --pf-c-label__content--before--BorderColor: #0066cc; + --pf-c-label--BackgroundColor: #0066cc; + --pf-c-label__content--Color: #fff; color: #fff; padding-top: 4px; padding-bottom: 4px; margin-left: 0; } + + .label-key .pf-c-label__content { + color: #00659c; + } + + .label-value .pf-c-label__content { + color: #fff; + } } diff --git a/web-console/src/Components/DataDisplay/Resource.scss b/web-console/src/Components/DataDisplay/Resource.scss index 74612313..c89f12ba 100644 --- a/web-console/src/Components/DataDisplay/Resource.scss +++ b/web-console/src/Components/DataDisplay/Resource.scss @@ -4,7 +4,7 @@ overflow: hidden; .key { display: inline-block; - color: #0303038c; + color: var(--pf-global--Color--200); min-width: 125px; padding-right: 5px; } diff --git a/web-console/src/Components/Form/Form.scss b/web-console/src/Components/Form/Form.scss index e2af26d2..c89d6c6e 100644 --- a/web-console/src/Components/Form/Form.scss +++ b/web-console/src/Components/Form/Form.scss @@ -10,7 +10,7 @@ .mc-form-header-label { font-size: var(--pf-global--FontSize--lg); font-style: normal; - color: #000000a0; + color: var(--pf-global--Color--200); } } @@ -41,6 +41,6 @@ --pf-l-split--m-gutter--MarginRight: 7px; } .field-title{ - color: #000000a0; + color: var(--pf-global--Color--200); } } diff --git a/web-console/src/Components/Graphs/Graphs.scss b/web-console/src/Components/Graphs/Graphs.scss index 0951b40e..0017fffa 100644 --- a/web-console/src/Components/Graphs/Graphs.scss +++ b/web-console/src/Components/Graphs/Graphs.scss @@ -1,5 +1,5 @@ .graph-title { - color: black; + color: var(--pf-global--Color--100); line-height: 1.4; margin: 0 0 10px 0; width: 100%; diff --git a/web-console/src/Components/Selector/Selector.scss b/web-console/src/Components/Selector/Selector.scss index 8c9aab6d..89a4dd60 100644 --- a/web-console/src/Components/Selector/Selector.scss +++ b/web-console/src/Components/Selector/Selector.scss @@ -12,9 +12,9 @@ padding: 10px; padding-right: 5px; padding-left: 10px; - color: var(--pf-global--palette--black-500); + color: var(--pf-global--Color--200); &:hover { - color: var(--pf-global--palette--black-600); + color: var(--pf-global--Color--100); } } diff --git a/web-console/src/Components/Widgets/ControlPanel/Common/Common.scss b/web-console/src/Components/Widgets/ControlPanel/Common/Common.scss index a3aa2a33..f8ad7800 100644 --- a/web-console/src/Components/Widgets/ControlPanel/Common/Common.scss +++ b/web-console/src/Components/Widgets/ControlPanel/Common/Common.scss @@ -1,5 +1,5 @@ .mc-control-panel { - background-color: var(--mc-widget-background-color); + background-color: transparent; .text { float: left; cursor: pointer; @@ -29,10 +29,10 @@ } .mixed-ctl-input-field { - --pf-c-popover__content--BackgroundColor: #fff; - --pf-c-popover__arrow--BackgroundColor: #fff; - border: 1px solid #2124276e; + --pf-c-popover__content--BackgroundColor: var(--pf-global--BackgroundColor--100); + --pf-c-popover__arrow--BackgroundColor: var(--pf-global--BackgroundColor--100); + border: 1px solid var(--pf-global--BorderColor--100); .pf-c-popover__arrow { - border: 1px solid #2124276e; + border: 1px solid var(--pf-global--BorderColor--100); } } diff --git a/web-console/src/Components/Widgets/ImagePanel/ImagePanel.scss b/web-console/src/Components/Widgets/ImagePanel/ImagePanel.scss index bf2b79ca..4f0670c5 100644 --- a/web-console/src/Components/Widgets/ImagePanel/ImagePanel.scss +++ b/web-console/src/Components/Widgets/ImagePanel/ImagePanel.scss @@ -13,12 +13,12 @@ } .value { - color: var(--pf-global--palette--black-850); + color: var(--pf-global--Color--100); font-size: 16px; } .value-timestamp { - color: var(--pf-global--palette--black-600); + color: var(--pf-global--Color--200); font-size: 14px; line-height: 0px; } diff --git a/web-console/src/Components/Widgets/Widget.scss b/web-console/src/Components/Widgets/Widget.scss index ec01928b..a7ec3435 100644 --- a/web-console/src/Components/Widgets/Widget.scss +++ b/web-console/src/Components/Widgets/Widget.scss @@ -15,7 +15,8 @@ .dashboard-widget { height: 100%; width: 100%; - background-color: var(--mc-widget-background-color); + background-color: transparent; + --pf-c-card--BackgroundColor: transparent; border-radius: 0px; position: relative; overflow: auto; @@ -41,7 +42,7 @@ } .dashboard-widget-title { padding: 5px 0px 5px 10px; - background-color: var(--mc-widget-title-background-color); + background-color: transparent; color: var(--mc-widget-title-color); } .dashboard-widget-title-edit { diff --git a/web-console/src/Constants/Common.js b/web-console/src/Constants/Common.js index 19fafcb1..67285860 100644 --- a/web-console/src/Constants/Common.js +++ b/web-console/src/Constants/Common.js @@ -23,7 +23,34 @@ export const URL_SOURCE_CODE = "https://github.com/mycontroller-org/server" export const DEFAULT_LANGUAGE = "en_GB" -export const DEFAULT_THEME = "default" +export const THEME_LIGHT = "light" +export const THEME_DARK = "dark" +export const THEME_SYSTEM = "system" +export const DEFAULT_THEME = THEME_SYSTEM + +export const THEME_OPTIONS = [ + { value: THEME_SYSTEM, label: "theme_system" }, + { value: THEME_LIGHT, label: "theme_light" }, + { value: THEME_DARK, label: "theme_dark" }, +] + +export const normalizeTheme = (value) => { + if (value === THEME_LIGHT || value === THEME_DARK || value === THEME_SYSTEM) { + return value + } + return THEME_SYSTEM +} + +export const resolveTheme = (selection) => { + const theme = normalizeTheme(selection) + if (theme !== THEME_SYSTEM) { + return theme + } + if (typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches) { + return THEME_DARK + } + return THEME_LIGHT +} export const SECURE_SHARE_PREFIX = "/secure_share/" export const INSECURE_SHARE_PREFIX = "/insecure_share/" diff --git a/web-console/src/Layout/Layout.jsx b/web-console/src/Layout/Layout.jsx index cfe195e3..816aedf2 100644 --- a/web-console/src/Layout/Layout.jsx +++ b/web-console/src/Layout/Layout.jsx @@ -23,6 +23,8 @@ import { PageSidebar, Popover, SkipToContent, + ToggleGroup, + ToggleGroupItem, Text, TextContent, TextVariants, @@ -42,8 +44,9 @@ import { GithubIcon, HelpIcon, InfoAltIcon, - LanguageIcon, - PaletteIcon, + DesktopIcon, + MoonIcon, + OutlinedSunIcon, PowerOffIcon, UnpluggedIcon, UserIcon, @@ -56,8 +59,16 @@ import { Redirect, Route, Switch } from "react-router-dom" import ErrorBoundary from "../Components/ErrorBoundary/ErrorBoundary" import { HeaderSpinner } from "../Components/Spinner/Spinner" import Toaster from "../Components/Toaster/Toaster" -import { URL_FORUM, URL_SOURCE_CODE } from "../Constants/Common" -import { languages } from "../i18n/languages" +import { + THEME_DARK, + THEME_LIGHT, + THEME_OPTIONS, + THEME_SYSTEM, + URL_FORUM, + URL_SOURCE_CODE, + normalizeTheme, +} from "../Constants/Common" +import { getLanguageCode, languages } from "../i18n/languages" import logoMain from "../Logo/mc-white.svg" import AboutPage from "../Pages/About/About" import { api } from "../Service/Api" @@ -68,7 +79,6 @@ import { clearAuth } from "../store/entities/auth" import { updateLocale } from "../store/entities/locale" import { updateTheme } from "../store/entities/theme" import { notificationDrawerToggle } from "../store/entities/notification" -import Theme from "../Theme/Theme" //import imgAvatar from "./imgAvatar.svg"; import "./Layout.scss" import NotificationContainer from "./NotificationContainer" @@ -78,37 +88,12 @@ class PageLayoutExpandableNav extends React.Component { isUserDropdownOpen: false, isHelpDropdownOpen: false, isLanguageDropdownOpen: false, - isThemeDropdownOpen: false, activeGroup: "grp-1", activeItem: "grp-1_itm-1", - themes: [], } componentDidMount() { wsConnect() - // load themes - this.fetchThemes() - } - - fetchThemes = () => { - const defaultThemes = [{ name: "default", description: "default theme" }] - const _page = { - limit: 15, // limits 15 entries - offset: 0, - filter: [{ k: "labels.gui_theme", o: "eq", v: "true" }], - // sort: { f: "id", o: "asc" }, - } - api.dataRepository - .list(_page) - .then((res) => { - const themes = res.data.data.map((theme) => { - return { name: theme.id, description: theme.description } - }) - this.setState({ themes: [...defaultThemes, ...themes] }) - }) - .catch((_e) => { - this.setState({ themes: defaultThemes }) - }) } componentWillUnmount() { @@ -151,18 +136,6 @@ class PageLayoutExpandableNav extends React.Component { }) } - onThemeDropdownToggle = (isThemeDropdownOpen) => { - this.setState({ - isThemeDropdownOpen, - }) - } - - onThemeDropdownSelect = (_event) => { - this.setState({ - isThemeDropdownOpen: !this.state.isThemeDropdownOpen, - }) - } - onNavSelect = (result) => { this.setState({ activeItem: result.itemId, @@ -218,7 +191,7 @@ class PageLayoutExpandableNav extends React.Component { websocketMessage, metricsDBDisabled, } = this.props - const { themes } = this.state + const selectedTheme = normalizeTheme(themeSelected) // selected menu let menuSelection = "" @@ -327,35 +300,29 @@ class PageLayoutExpandableNav extends React.Component { , ] + const currentLanguage = languages.find((l) => l.lng === languageSelected) || languages[0] const languageItems = languages.map((l) => { - const lngSelected = l.lng === languageSelected ? "language_selected" : "" + const lngSelected = l.lng === currentLanguage.lng ? "language_selected" : "" return ( this.props.updateLocale({ language: l.lng })} > - - {l.flag} - - {l.title} + + {l.flag} + {l.title} + {getLanguageCode(l.lng)} + ) }) - const themeDropdownItems = themes.map((theme) => { - const thSelected = theme.name === themeSelected ? "language_selected" : "" - return ( - this.props.updateTheme({ theme: theme.name })} - description={theme.description} - > - {theme.name} - - ) - }) + const themeIcons = { + [THEME_SYSTEM]: , + [THEME_LIGHT]: , + [THEME_DARK]: , + } const userDropdownItems = [ @@ -399,8 +366,43 @@ class PageLayoutExpandableNav extends React.Component { {this.props.showGlobalSpinner ? : null} - + {websocketStatus} + + + {THEME_OPTIONS.map((theme) => ( + + {themeIcons[theme.value]} + + } + aria-label={t(theme.label)} + isSelected={selectedTheme === theme.value} + onChange={() => this.props.updateTheme({ theme: theme.value })} + /> + ))} + + + + + + {currentLanguage.flag} {getLanguageCode(currentLanguage.lng)} + + + } + dropdownItems={languageItems} + /> + } @@ -427,41 +430,6 @@ class PageLayoutExpandableNav extends React.Component { dropdownItems={helpDropdownItems} /> - - } - /> - } - dropdownItems={languageItems} - /> - - - - } - /> - } - dropdownItems={themeDropdownItems} - /> - - - text { + fill: var(--pf-global--Color--100); + } .pf-topology__node.pf-m-selected { .topology-label-id, .topology-label-value, @@ -82,7 +86,7 @@ padding: 8px 12px 0; } .topology-sidebar-subtitle { - color: #6a6e73; + color: var(--pf-global--Color--200); font-size: 13px; margin-top: 2px; } @@ -152,7 +156,7 @@ .topology-sidebar-id-name, .topology-sidebar-payload-time { flex-shrink: 0; - color: #6a6e73; + color: var(--pf-global--Color--200); font-size: 12px; font-weight: 400; white-space: nowrap; diff --git a/web-console/src/Theme/Theme.jsx b/web-console/src/Theme/Theme.jsx index 70429f57..b94b7569 100644 --- a/web-console/src/Theme/Theme.jsx +++ b/web-console/src/Theme/Theme.jsx @@ -1,66 +1,58 @@ import React from "react" import { connect } from "react-redux" -import { DEFAULT_THEME } from "../Constants/Common" -import { api } from "../Service/Api" -import { getValue } from "../Util/Util" - -// default Colors -const defaultTheme = { - "--mc-background-color": "#fff", - "--mc-components-background-color": "#fff", - "--mc-dashboard-background-color": "#f7f7f7", - "--mc-widget-background-color": "#fff", - "--mc-widget-title-background-color": "#f6f6f6", - "--mc-widget-utilization-title-color": "var(--pf-global--palette--black-600)", - "--mc-widget-utilization-value-color": "var(--pf-global--palette--black-800)", - "--mc-widget-utilization-unit-color": "var(--pf-global--palette--black-600)", - "--mc-widget-utilization-function-color": "var(--pf-global--palette--black-400)", - "--mc-widget-title-color": "#434343", - "--mc-timestamp-color": "var(--pf-global--palette--black-600)", -} +import { THEME_SYSTEM, normalizeTheme } from "../Constants/Common" +import { updateTheme } from "../store/entities/theme" +import { applyThemeClass, loadStoredTheme, saveStoredTheme } from "./themeStorage" class Theme extends React.Component { - state = { - themeSettings: {}, - } + mediaQuery = null componentDidMount() { - this.loadThemeData(this.props.themeSelected) + const stored = loadStoredTheme() + const normalized = stored || normalizeTheme(this.props.themeSelected) + if (normalized !== this.props.themeSelected) { + this.props.updateTheme({ theme: normalized }) + } + saveStoredTheme(normalized) + this.applySelection(normalized) + this.mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") + if (this.mediaQuery.addEventListener) { + this.mediaQuery.addEventListener("change", this.onSystemChange) + } else if (this.mediaQuery.addListener) { + this.mediaQuery.addListener(this.onSystemChange) + } } componentDidUpdate(prevProps) { if (prevProps.themeSelected !== this.props.themeSelected) { - this.loadThemeData(this.props.themeSelected) + this.applySelection(this.props.themeSelected) + } + } + + componentWillUnmount() { + if (!this.mediaQuery) { + return + } + if (this.mediaQuery.removeEventListener) { + this.mediaQuery.removeEventListener("change", this.onSystemChange) + } else if (this.mediaQuery.removeListener) { + this.mediaQuery.removeListener(this.onSystemChange) } } - loadThemeData = (themeSelected = DEFAULT_THEME) => { - if (themeSelected === DEFAULT_THEME) { - this.setState({ themeSettings: defaultTheme }) - } else { - api.dataRepository - .get(themeSelected) - .then((res) => { - const themeData = getValue(res, "data.data", defaultTheme) - this.setState({ themeSettings: themeData }) - }) - .catch((_e) => { - this.setState({ themeSettings: defaultTheme }) - }) + onSystemChange = () => { + if (normalizeTheme(this.props.themeSelected) === THEME_SYSTEM) { + applyThemeClass(THEME_SYSTEM) } } - updateTheme = (themeSettings = {}) => { - const settings = { ...defaultTheme, ...themeSettings } - const keys = Object.keys(settings) - keys.map((key) => { - document.documentElement.style.setProperty(key, settings[key]) - }) + applySelection = (selection) => { + const normalized = normalizeTheme(selection) + saveStoredTheme(normalized) + applyThemeClass(normalized) } render() { - const { themeSettings } = this.state - this.updateTheme(themeSettings) return null } } @@ -69,4 +61,4 @@ const mapStateToProps = (state) => ({ themeSelected: state.entities.theme.selection, }) -export default connect(mapStateToProps, null)(Theme) +export default connect(mapStateToProps, { updateTheme })(Theme) diff --git a/web-console/src/Theme/themeStorage.js b/web-console/src/Theme/themeStorage.js new file mode 100644 index 00000000..e0e2a750 --- /dev/null +++ b/web-console/src/Theme/themeStorage.js @@ -0,0 +1,29 @@ +import { DEFAULT_THEME, THEME_DARK, normalizeTheme, resolveTheme } from "../Constants/Common" + +export const THEME_STORAGE_KEY = "mc-theme" + +export const loadStoredTheme = () => { + try { + const raw = window.localStorage.getItem(THEME_STORAGE_KEY) + if (raw == null || raw === "") { + return null + } + return normalizeTheme(raw) + } catch (_e) { + return null + } +} + +export const saveStoredTheme = (theme) => { + try { + window.localStorage.setItem(THEME_STORAGE_KEY, normalizeTheme(theme)) + } catch (_e) { + // private mode / quota + } +} + +export const applyThemeClass = (selection) => { + document.documentElement.classList.toggle("pf-theme-dark", resolveTheme(selection) === THEME_DARK) +} + +export const initialTheme = () => loadStoredTheme() || DEFAULT_THEME diff --git a/web-console/src/i18n/languages.js b/web-console/src/i18n/languages.js index 6bd9d7cd..a03c3128 100644 --- a/web-console/src/i18n/languages.js +++ b/web-console/src/i18n/languages.js @@ -132,3 +132,12 @@ export const getLanguage = (lng = DEFAULT_LANGUAGE) => { } } } + +export const getLanguageCode = (lng = DEFAULT_LANGUAGE) => { + if (lng === "en_GB") { + return "EN-UK" + } + return String(lng || DEFAULT_LANGUAGE) + .replace("_", "-") + .toUpperCase() +} diff --git a/web-console/src/index.js b/web-console/src/index.js index b023e46e..69420943 100644 --- a/web-console/src/index.js +++ b/web-console/src/index.js @@ -9,6 +9,9 @@ import App from "./App" import "./i18n/i18n" import "./index.scss" import * as serviceWorker from "./serviceWorker" +import { applyThemeClass, initialTheme } from "./Theme/themeStorage" + +applyThemeClass(initialTheme()) ReactDOM.render( // diff --git a/web-console/src/store/entities/theme.js b/web-console/src/store/entities/theme.js index 123a9a67..074e639b 100644 --- a/web-console/src/store/entities/theme.js +++ b/web-console/src/store/entities/theme.js @@ -1,14 +1,15 @@ import { createSlice } from "@reduxjs/toolkit" -import { DEFAULT_THEME } from "../../Constants/Common" +import { normalizeTheme } from "../../Constants/Common" +import { initialTheme } from "../../Theme/themeStorage" const slice = createSlice({ name: "theme", initialState: { - selection: DEFAULT_THEME, + selection: initialTheme(), }, reducers: { updateTheme: (theme, action) => { - theme.selection = action.payload.theme + theme.selection = normalizeTheme(action.payload.theme) }, }, }) diff --git a/web-console/src/theme.scss b/web-console/src/theme.scss index 442f8ec9..a7f339bd 100644 --- a/web-console/src/theme.scss +++ b/web-console/src/theme.scss @@ -1,19 +1,15 @@ -// include/update only color scheme -// for complete details supported variables -// visit: https://www.patternfly.org/v4/developer-resources/global-css-variables - +// PatternFly light is the default. Dark uses html.pf-theme-dark (in base.css). :root { - --mc-background-color: #fff; - --mc-components-background-color: #fff; - --mc-dashboard-background-color: #f7f7f7; - --mc-widget-background-color: #fff; - --mc-widget-title-background-color: #f6f6f6; - --mc-widget-title-color: #434343; - --mc-timestamp-color: var(--pf-global--palette--black-600); - --mc-widget-utilization-title-color: var(--pf-global--palette--black-600); - --mc-widget-utilization-value-color: var(--pf-global--palette--black-800); - --mc-widget-utilization-unit-color: var(--pf-global--palette--black-600); - --mc-widget-utilization-function-color: var(--pf-global--palette--black-400); + --mc-background-color: var(--pf-global--BackgroundColor--100); + --mc-components-background-color: var(--pf-global--BackgroundColor--100); + --mc-widget-background-color: var(--pf-global--BackgroundColor--100); + --mc-widget-title-background-color: var(--pf-global--BackgroundColor--200); + --mc-widget-title-color: var(--pf-global--Color--100); + --mc-timestamp-color: var(--pf-global--Color--200); + --mc-widget-utilization-title-color: var(--pf-global--Color--200); + --mc-widget-utilization-value-color: var(--pf-global--Color--100); + --mc-widget-utilization-unit-color: var(--pf-global--Color--200); + --mc-widget-utilization-function-color: var(--pf-global--Color--300); } .pf-c-card { @@ -35,3 +31,41 @@ .pf-c-modal-box { --pf-c-modal-box--BackgroundColor: var(--mc-background-color); } + +:root:where(.pf-theme-dark) { + color: var(--pf-global--Color--100); + + .label-pair .label-key { + --pf-c-label__content--before--BorderColor: #004d8a; + --pf-c-label--BackgroundColor: #002952; + --pf-c-label__content--Color: #73bcf7; + color: #73bcf7; + } + + .label-pair .label-key .pf-c-label__content { + color: #73bcf7; + } + + .label-pair .label-value { + --pf-c-label__content--before--BorderColor: var(--pf-global--primary-color--100); + --pf-c-label--BackgroundColor: var(--pf-global--primary-color--100); + --pf-c-label__content--Color: #fff; + color: #fff; + } + + .label-pair .label-value .pf-c-label__content { + color: #fff; + } + + .pf-c-description-list__term, + .pf-c-description-list__text, + .pf-c-content, + .pf-c-title, + .graph-title { + color: var(--pf-global--Color--100); + } + + .pf-c-description-list__description { + color: var(--pf-global--Color--200); + } +}