From cbb2facc8771ca266ea7e49ca2e73ca2dd84f43e Mon Sep 17 00:00:00 2001 From: nicolargo Date: Sun, 10 Dec 2023 19:27:02 +0100 Subject: [PATCH] Unit tests are ok BUT the WebUI has a lot of issue: plugin disapear, traceback... Perhaps issue when stats are refreched ? --- glances/outputs/glances_restful_api.py | 122 +++++++++++------- .../static/js/components/plugin-process.vue | 6 +- .../js/components/plugin-processlist.vue | 8 +- glances/outputs/static/public/glances.js | 2 +- glances/outputs/static/templates/index.html | 22 ++++ .../outputs/static/templates/index.html.tpl | 22 ---- glances/processes.py | 2 +- unitest-restful.py | 15 ++- 8 files changed, 118 insertions(+), 81 deletions(-) create mode 100644 glances/outputs/static/templates/index.html delete mode 100644 glances/outputs/static/templates/index.html.tpl diff --git a/glances/outputs/glances_restful_api.py b/glances/outputs/glances_restful_api.py index 54c4f908..c8e7094a 100644 --- a/glances/outputs/glances_restful_api.py +++ b/glances/outputs/glances_restful_api.py @@ -20,19 +20,10 @@ from urllib.parse import urljoin # from typing import Annotated from typing_extensions import Annotated -from glances.globals import json_dumps from glances.timer import Timer from glances.logger import logger # FastAPI import - -# TODO: not sure import is needed -try: - import jinja2 -except ImportError: - logger.critical('Jinja2 import error. Glances cannot start in web server mode.') - sys.exit(2) - try: from fastapi import FastAPI, Depends, HTTPException, status, APIRouter, Request from fastapi.security import HTTPBasic, HTTPBasicCredentials @@ -78,7 +69,7 @@ class GlancesRestfulApi(object): # Load configuration file self.load_config(config) - # Set the bind URL (only used for log information purpose) + # Set the bind URL self.bind_url = urljoin('http://{}:{}/'.format(self.args.bind_address, self.args.port), self.url_prefix) @@ -95,7 +86,6 @@ class GlancesRestfulApi(object): # Set path for WebUI self.STATIC_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/public') - # TEMPLATE_PATH.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/templates')) self.TEMPLATE_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/templates') self._templates = Jinja2Templates(directory=self.TEMPLATE_PATH) @@ -103,7 +93,10 @@ class GlancesRestfulApi(object): # https://fastapi.tiangolo.com/tutorial/cors/ self._app.add_middleware( CORSMiddleware, - allow_origins=["*"], + # allow_origins=["*"], + allow_origins=[ + self.bind_url + ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -140,7 +133,7 @@ class GlancesRestfulApi(object): # TODO: the password comparaison is not working for the moment. # if the password is wrong, authentication is working... - # Perahps because the password is hashed in the GlancesPassword class + # Perhaps because the password is hashed in the GlancesPassword class # and the one given by creds.password is not hashed ? def authentication(self, creds: Annotated[HTTPBasicCredentials, Depends(security)]): """Check if a username/password combination is valid.""" @@ -168,21 +161,24 @@ class GlancesRestfulApi(object): status_code=status.HTTP_200_OK, response_class=ORJSONResponse, endpoint=self._api_status) + router.add_api_route('/api/%s/config' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_config) - router.add_api_route('/api/%s/config/{item}' % self.API_VERSION, + router.add_api_route('/api/%s/config/{section}' % self.API_VERSION, response_class=ORJSONResponse, - endpoint=self._api_config_item) + endpoint=self._api_config_section) + router.add_api_route('/api/%s/config/{section}/{item}' % self.API_VERSION, + response_class=ORJSONResponse, + endpoint=self._api_config_section_item) + router.add_api_route('/api/%s/args' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_args) router.add_api_route('/api/%s/args/{item}' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_args_item) - router.add_api_route('/api/%s/help' % self.API_VERSION, - response_class=ORJSONResponse, - endpoint=self._api_help) + router.add_api_route('/api/%s/pluginslist' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_plugins) @@ -195,6 +191,10 @@ class GlancesRestfulApi(object): router.add_api_route('/api/%s/all/views' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_all_views) + + router.add_api_route('/api/%s/help' % self.API_VERSION, + response_class=ORJSONResponse, + endpoint=self._api_help) router.add_api_route('/api/%s/{plugin}' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api) @@ -204,7 +204,7 @@ class GlancesRestfulApi(object): router.add_api_route('/api/%s/{plugin}/history/{nb}' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_history) - router.add_api_route('/api/%s/{plugin}/top/' % self.API_VERSION, + router.add_api_route('/api/%s/{plugin}/top/{nb}' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_top) router.add_api_route('/api/%s/{plugin}/limits' % self.API_VERSION, @@ -232,18 +232,13 @@ class GlancesRestfulApi(object): # WEB UI if not self.args.disable_webui: - # Template + # Template for the root index.html file router.add_api_route('/', response_class=HTMLResponse, endpoint=self._index) - # TODO: to be migrated to another route - # router.add_api_route('/{refresh_time}', - # endpoint=self._index) - # Statics files - # self._app.mount("/static", StaticFiles(directory=self.STATIC_PATH), name="static") - self._app.mount("/", + self._app.mount("/static", StaticFiles(directory=self.STATIC_PATH), name="static") @@ -282,30 +277,27 @@ class GlancesRestfulApi(object): logger.critical('Error: Can not ran Glances Web server ({})'.format(e)) def end(self): - """End the bottle.""" + """End the Web server""" logger.info("Close the Web server") - # TODO: close FastAPI instance gracefully - # self._app.close() - # if self.url_prefix != '/': - # self.main_app.close() - # Example from FastAPI documentation - # @app.get("/", response_class=HTMLResponse) - # def home(request: Request): - # return templates.TemplateResponse("index.html", {"request": request}) + def _index(self, request: Request): + """Return main index.html (/) file. + Parameters are available through the request object. + Example: http://localhost:61208/?refresh=5 + """ - def _index(self, refresh_time=None): - """Return main index.html (/) file.""" - - if refresh_time is None or refresh_time < 1: - refresh_time = int(self.args.time) + refresh_time = request.query_params.get('refresh', + default=max(1, int(self.args.time))) # Update the stat self.__update__() # Display - # return template("index.html", refresh_time=refresh_time) - return self.templates.TemplateResponse("index.html") + return self._templates.TemplateResponse("index.html", + { + "request": request, + "refresh_time": refresh_time, + }) def _api_status(self): """Glances API RESTful implementation. @@ -662,29 +654,61 @@ class GlancesRestfulApi(object): return ORJSONResponse(args_json) - def _api_config_item(self, item): + def _api_config_section(self, section): """Glances API RESTful implementation. - Return the JSON representation of the Glances configuration item + Return the JSON representation of the Glances configuration section HTTP/200 if OK HTTP/400 if item is not found HTTP/404 if others error """ config_dict = self.config.as_dict() - if item not in config_dict: + if section not in config_dict: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Unknown configuration item %s" % item) + detail="Unknown configuration item %s" % section) try: # Get the RAW value of the config' dict - args_json = config_dict[item] + ret_section = config_dict[section] except Exception as e: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail="Cannot get config item (%s)" % str(e)) + detail="Cannot get config section %s (%s)" % (section, str(e))) - return ORJSONResponse(args_json) + return ORJSONResponse(ret_section) + + def _api_config_section_item(self, section, item): + """Glances API RESTful implementation. + + Return the JSON representation of the Glances configuration section/item + HTTP/200 if OK + HTTP/400 if item is not found + HTTP/404 if others error + """ + config_dict = self.config.as_dict() + if section not in config_dict: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Unknown configuration item %s" % section) + + try: + # Get the RAW value of the config' dict section + ret_section = config_dict[section] + except Exception as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Cannot get config section %s (%s)" % (section, str(e))) + + try: + # Get the RAW value of the config' dict item + ret_item = ret_section[item] + except Exception as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Cannot get item %s in config section %s (%s)" % (item, section, str(e))) + + return ORJSONResponse(ret_item) def _api_args(self): """Glances API RESTful implementation. diff --git a/glances/outputs/static/js/components/plugin-process.vue b/glances/outputs/static/js/components/plugin-process.vue index 36cf2500..05b053fb 100644 --- a/glances/outputs/static/js/components/plugin-process.vue +++ b/glances/outputs/static/js/components/plugin-process.vue @@ -64,13 +64,13 @@ export default { } function getColumnLabel(value) { const labels = { - io_counters: 'disk IO', cpu_percent: 'CPU consumption', memory_percent: 'memory consumption', - cpu_times: 'process time', username: 'user name', - name: 'process name', timemillis: 'process time', + cpu_times: 'process time', + io_counters: 'disk IO', + name: 'process name', None: 'None' }; return labels[value] || value; diff --git a/glances/outputs/static/js/components/plugin-processlist.vue b/glances/outputs/static/js/components/plugin-processlist.vue index 462fce61..71388066 100644 --- a/glances/outputs/static/js/components/plugin-processlist.vue +++ b/glances/outputs/static/js/components/plugin-processlist.vue @@ -78,10 +78,10 @@
{{ process.memory_percent == -1 ? '?' : $filters.number(process.memory_percent, 1) }}
-