Skip to content

Commit

Permalink
use taipy-assets favicon (#1453)
Browse files Browse the repository at this point in the history
* use taipy-assets favicon
resolves #1327
allow to change favicon dynamically
resolves #1244

* with doc

* doc

* test

* Fab's comments

* allow to change favicon for one state

* fab's comments

* favicon

* mypy

* trying to fix cross-env ...

* trying to fix cross-env ...

* trying to fix cross-env ...

* do not run test if cache hit

* noncoverage either if cache hit

* class name

---------

Co-authored-by: Fred Lefévère-Laoide <[email protected]>
  • Loading branch information
FredLL-Avaiga and Fred Lefévère-Laoide authored Jun 27, 2024
1 parent d68654c commit 16e90fb
Show file tree
Hide file tree
Showing 11 changed files with 127 additions and 23 deletions.
12 changes: 7 additions & 5 deletions .github/workflows/frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,15 @@ jobs:
- if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
run: npm run build --if-present

- run: npm test
- if: steps.cache-gui-fe-build.outputs.cache-hit != 'true'
run: npm test

- name: Code coverage
if: matrix.os == 'ubuntu-latest' && github.event_name == 'pull_request'
uses: artiomtr/jest-coverage-report-action@v2.2.6
if: matrix.os == 'ubuntu-latest' && github.event_name == 'pull_request' && steps.cache-gui-fe-build.outputs.cache-hit != 'true'
uses: artiomtr/jest-coverage-report-action@v2.3.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
threshold: "80"
working-directory: "frontend/taipy-gui"
skip-step: "install"
working-directory: "./frontend/taipy-gui"
skip-step: install
annotations: failed-tests
Binary file modified frontend/taipy-gui/public/favicon.ico
Binary file not shown.
Binary file modified frontend/taipy-gui/public/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion frontend/taipy-gui/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="manifest" href="manifest.json" />
<link rel="icon" type="image/png" href="{{favicon}}" />
<link rel="icon" type="image/png" href="favicon.png" class="taipy-favicon" data-url="{{favicon}}" />
<link rel="apple-touch-icon" href="favicon.png" />
<title>{{title}}</title>
<script>{%- if config -%}
Expand Down
5 changes: 4 additions & 1 deletion frontend/taipy-gui/src/context/taipyReducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { stylekitModeThemes, stylekitTheme } from "../themes/stylekit";
import { getBaseURL, TIMEZONE_CLIENT } from "../utils";
import { parseData } from "../utils/dataFormat";
import { MenuProps } from "../utils/lov";
import { getLocalStorageValue, IdMessage, storeClientId } from "./utils";
import { changeFavicon, getLocalStorageValue, IdMessage, storeClientId } from "./utils";
import { ligthenPayload, sendWsMessage, TAIPY_CLIENT_ID, WsMessage } from "./wsUtils";

enum Types {
Expand Down Expand Up @@ -228,6 +228,8 @@ const messageToAction = (message: WsMessage) => {
return createPartialAction((message as unknown as Record<string, string>).name, true);
} else if (message.type === "ACK") {
return createAckAction((message as unknown as IdMessage).id);
} else if (message.type === "FV") {
changeFavicon((message.payload as Record<string, string>)?.value);
}
}
return {} as TaipyBaseAction;
Expand Down Expand Up @@ -278,6 +280,7 @@ export const initializeWebSocket = (socket: Socket | undefined, dispatch: Dispat
socket.on("message", getWsMessageListener(dispatch));
// only now does the socket tries to open/connect
socket.connect();
changeFavicon();
}
};

Expand Down
20 changes: 20 additions & 0 deletions frontend/taipy-gui/src/context/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import axios from "axios";
import { TAIPY_CLIENT_ID } from "./wsUtils";

export const getLocalStorageValue = <T = string>(key: string, defaultValue: T, values?: T[]) => {
Expand All @@ -10,3 +11,22 @@ export const storeClientId = (id: string) => localStorage && localStorage.setIte
export interface IdMessage {
id: string;
}

export const changeFavicon = (url?: string) => {
const link: HTMLLinkElement | null = document.querySelector("link.taipy-favicon");
if (link) {
const { url: taipyUrl } = link.dataset;
const fetchUrl = url || (taipyUrl as string);
axios
.get(fetchUrl)
.then(() => {
link.href = fetchUrl;
})
.catch((error) => {
if (fetchUrl !== taipyUrl) {
link.href = taipyUrl as string;
}
console.log(error);
});
}
};
3 changes: 2 additions & 1 deletion frontend/taipy-gui/src/context/wsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export type WsMessageType =
| "GMC"
| "GDT"
| "AID"
| "GR";
| "GR"
| "FV";

export interface WsMessage {
type: WsMessageType;
Expand Down
60 changes: 45 additions & 15 deletions taipy/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ class Gui:
__ROBOTO_FONT = "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
__DOWNLOAD_ACTION = "__Taipy__download_csv"
__DOWNLOAD_DELETE_ACTION = "__Taipy__download_delete_csv"
__DEFAULT_FAVICON_URL = "https://raw.githubusercontent.com/Avaiga/taipy-assets/develop/favicon.png"

__RE_HTML = re.compile(r"(.*?)\.html$")
__RE_MD = re.compile(r"(.*?)\.md$")
Expand Down Expand Up @@ -322,6 +323,7 @@ def __init__(
self.__evaluator: _Evaluator = None # type: ignore
self.__adapter = _Adapter()
self.__directory_name_of_pages: t.List[str] = []
self.__favicon: t.Optional[t.Union[str, Path]] = None

# default actions
self.on_action: t.Optional[t.Callable] = None
Expand Down Expand Up @@ -1117,9 +1119,7 @@ def __handle_ws_get_module_context(self, payload: t.Any):
# Get Module Context
if mc := self._get_page_context(page_path):
page_renderer = self._get_page(page_path)._renderer
self._bind_custom_page_variables(
page_renderer, self._get_client_id()
)
self._bind_custom_page_variables(page_renderer, self._get_client_id())
# get metadata if there is one
metadata: t.Dict[str, t.Any] = {}
if hasattr(page_renderer, "_metadata"):
Expand Down Expand Up @@ -1227,7 +1227,7 @@ def __send_ws(self, payload: dict, allow_grouping=True, send_back_only=False) ->
def __broadcast_ws(self, payload: dict, client_id: t.Optional[str] = None):
try:
to = list(self.__get_sids(client_id)) if client_id else []
self._server._ws.emit("message", payload, to=to if to else None)
self._server._ws.emit("message", payload, to=to if to else None, include_self=True)
time.sleep(0.001)
except Exception as e: # pragma: no cover
_warn(f"Exception raised in WebSocket communication in '{self.__frame.f_code.co_name}'", e)
Expand Down Expand Up @@ -1315,9 +1315,19 @@ def __send_ws_update_with_dict(self, modified_values: dict) -> None:
else:
self.__send_ws({"type": _WsType.MULTIPLE_UPDATE.value, "payload": payload})

def __send_ws_broadcast(self, var_name: str, var_value: t.Any, client_id: t.Optional[str] = None):
def __send_ws_broadcast(
self,
var_name: str,
var_value: t.Any,
client_id: t.Optional[str] = None,
message_type: t.Optional[_WsType] = None,
):
self.__broadcast_ws(
{"type": _WsType.UPDATE.value, "name": _get_broadcast_var_name(var_name), "payload": {"value": var_value}},
{
"type": _WsType.UPDATE.value if message_type is None else message_type.value,
"name": _get_broadcast_var_name(var_name),
"payload": {"value": var_value},
},
client_id,
)

Expand Down Expand Up @@ -1977,7 +1987,13 @@ def __bind_local_func(self, name: str):
def load_config(self, config: Config) -> None:
self._config._load(config)

def _broadcast(self, name: str, value: t.Any, client_id: t.Optional[str] = None):
def _broadcast(
self,
name: str,
value: t.Any,
client_id: t.Optional[str] = None,
message_type: t.Optional[_WsType] = None,
):
"""NOT DOCUMENTED
Send the new value of a variable to all connected clients.
Expand All @@ -1986,7 +2002,7 @@ def _broadcast(self, name: str, value: t.Any, client_id: t.Optional[str] = None)
value: The value (must be serializable to the JSON format).
client_id: The client id (broadcast to all client if None)
"""
self.__send_ws_broadcast(name, value, client_id)
self.__send_ws_broadcast(name, value, client_id, message_type)

def _broadcast_all_clients(self, name: str, value: t.Any):
try:
Expand Down Expand Up @@ -2411,7 +2427,7 @@ def __register_blueprint(self):
static_folder=_webapp_path,
template_folder=_webapp_path,
title=self._get_config("title", "Taipy App"),
favicon=self._get_config("favicon", "favicon.png"),
favicon=self._get_config("favicon", Gui.__DEFAULT_FAVICON_URL),
root_margin=self._get_config("margin", None),
scripts=scripts,
styles=styles,
Expand Down Expand Up @@ -2440,8 +2456,7 @@ def run(
async_mode: str = "gevent",
**kwargs,
) -> t.Optional[Flask]:
"""
Start the server that delivers pages to web clients.
"""Start the server that delivers pages to web clients.
Once you enter `run()`, users can run web browsers and point to the web server
URL that `Gui` serves. The default is to listen to the *localhost* address
Expand Down Expand Up @@ -2593,8 +2608,7 @@ def run(
)

def reload(self): # pragma: no cover
"""
Reload the web server.
"""Reload the web server.
This function reloads the underlying web server only in the situation where
it was run in a separated thread: the *run_in_thread* parameter to the
Expand All @@ -2607,8 +2621,7 @@ def reload(self): # pragma: no cover
_TaipyLogger._get_logger().info("Gui server has been reloaded.")

def stop(self):
"""
Stop the web server.
"""Stop the web server.
This function stops the underlying web server only in the situation where
it was run in a separated thread: the *run_in_thread* parameter to the
Expand All @@ -2621,3 +2634,20 @@ def stop(self):

def _get_autorization(self, client_id: t.Optional[str] = None, system: t.Optional[bool] = False):
return contextlib.nullcontext()

def set_favicon(self, favicon_path: t.Union[str, Path], state: t.Optional[State] = None):
"""Change the favicon for all clients.
This function dynamically changes the favicon of Taipy GUI pages for all connected client.
favicon_path can be an URL (relative or not) or a file path.
TODO The *favicon* parameter to `(Gui.)run()^` can also be used to change
the favicon when the application starts.
"""
if state or self.__favicon != favicon_path:
if not state:
self.__favicon = favicon_path
url = self._get_content("__taipy_favicon", favicon_path, True)
self._broadcast(
"taipy_favicon", url, self._get_client_id() if state else None, message_type=_WsType.FAVICON
)
12 changes: 12 additions & 0 deletions taipy/gui/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import typing as t
from contextlib import nullcontext
from operator import attrgetter
from pathlib import Path
from types import FrameType

from flask import has_app_context
Expand Down Expand Up @@ -83,6 +84,7 @@ def change_variable(state):
"broadcast",
"get_gui",
"refresh",
"set_favicon",
"_set_context",
"_notebook_context",
"_get_placeholder",
Expand Down Expand Up @@ -243,3 +245,13 @@ def __enter__(self):

def __exit__(self, exc_type, exc_value, traceback):
return super().__getattribute__(State.__attrs[0]).__exit__(exc_type, exc_value, traceback)

def set_favicon(self, favicon_path: t.Union[str, Path]):
"""Change the favicon for the client of this state.
This function dynamically changes the favicon of Taipy GUI pages for a specific client.
favicon_path can be an URL (relative or not) or a file path.
TODO The *favicon* parameter to `(Gui.)run()^` can also be used to change
the favicon when the application starts.
"""
super().__getattribute__(State.__gui_attr).set_favicon(favicon_path, self)
1 change: 1 addition & 0 deletions taipy/gui/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class _WsType(Enum):
GET_MODULE_CONTEXT = "GMC"
GET_DATA_TREE = "GDT"
GET_ROUTES = "GR"
FAVICON = "FV"


NumberTypes = {"int", "int64", "float", "float64"}
Expand Down
35 changes: 35 additions & 0 deletions tests/gui/gui_specific/test_favicon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright 2021-2024 Avaiga Private Limited
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.

import inspect
import warnings

from taipy.gui import Gui, Markdown


def test_favicon(gui: Gui, helpers):

with warnings.catch_warnings(record=True):
gui._set_frame(inspect.currentframe())
gui.add_page("test", Markdown("#This is a page"))
gui.run(run_server=False)
client = gui._server.test_client()
# WS client and emit
ws_client = gui._server._ws.test_client(gui._server.get_flask())
# Get the jsx once so that the page will be evaluated -> variable will be registered
sid = helpers.create_scope_and_get_sid(gui)
client.get(f"/taipy-jsx/test/?client_id={sid}")
gui.set_favicon("https://newfavicon.com/favicon.png")
# assert for received message (message that would be sent to the front-end client)
msgs = ws_client.get_received()
assert msgs
assert msgs[0].get("args", {}).get("type", None) == "FV"
assert msgs[0].get("args", {}).get("payload", {}).get("value", None) == "https://newfavicon.com/favicon.png"

0 comments on commit 16e90fb

Please sign in to comment.