diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py b/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py index 72355fe97..ce8a3970b 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py @@ -185,13 +185,19 @@ def create_app() -> flask.app.Flask: return app # type: ignore +def get_version_info_data() -> dict[str, Any]: + version_info_data_dict = {} + if os.path.isfile("version_info.json"): + with open("version_info.json") as f: + version_info_data_dict = json.load(f) + return version_info_data_dict + + def _setup_prometheus_metrics(app: flask.app.Flask, connexion_app: connexion.apps.flask_app.FlaskApp) -> None: metrics = ConnexionPrometheusMetrics(connexion_app) app.config["PROMETHEUS_METRICS"] = metrics - if os.path.isfile("version_info.json"): - version_info_data = {} - with open("version_info.json") as f: - version_info_data = json.load(f) + version_info_data = get_version_info_data() + if len(version_info_data) > 0: # prometheus does not allow periods in key names version_info_data_normalized = {k.replace(".", "_"): v for k, v in version_info_data.items()} metrics.info("version_info", "Application Version Info", **version_info_data_normalized) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index a43736292..7dcb81ce8 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -162,6 +162,19 @@ paths: schema: $ref: "#/components/schemas/OkTrue" + /debug/version-info: + get: + operationId: spiffworkflow_backend.routes.debug_controller.version_info + summary: Returns information about the version of the application + tags: + - Status + responses: + "200": + description: Returns version info if it exists. + content: + application/json: + schema: + $ref: "#/components/schemas/OkTrue" /active-users/updates/{last_visited_identifier}: parameters: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py index a71157458..d61fa0850 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py @@ -18,13 +18,13 @@ def setup_database_uri(app: Flask) -> None: if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_URI") is None: database_name = f"spiffworkflow_backend_{app.config['ENV_IDENTIFIER']}" if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "sqlite": - app.config[ - "SQLALCHEMY_DATABASE_URI" - ] = f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3" + app.config["SQLALCHEMY_DATABASE_URI"] = ( + f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3" + ) elif app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "postgres": - app.config[ - "SQLALCHEMY_DATABASE_URI" - ] = f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}" + app.config["SQLALCHEMY_DATABASE_URI"] = ( + f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}" + ) else: # use pswd to trick flake8 with hardcoded passwords db_pswd = app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_PASSWORD") diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py index 078925e70..1668565c9 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py @@ -129,9 +129,9 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel): def serialized_with_metadata(self) -> dict[str, Any]: process_instance_attributes = self.serialized process_instance_attributes["process_metadata"] = self.process_metadata - process_instance_attributes[ - "process_model_with_diagram_identifier" - ] = self.process_model_with_diagram_identifier + process_instance_attributes["process_model_with_diagram_identifier"] = ( + self.process_model_with_diagram_identifier + ) return process_instance_attributes @property diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/debug_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/debug_controller.py index 29a81447b..f9507af6a 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/debug_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/debug_controller.py @@ -1,6 +1,13 @@ """APIs for dealing with process groups, process models, and process instances.""" +from flask import make_response from flask.wrappers import Response +from spiffworkflow_backend import get_version_info_data + def test_raise_error() -> Response: raise Exception("This exception was generated by /debug/test-raise-error for testing purposes. Please ignore.") + + +def version_info() -> Response: + return make_response(get_version_info_data(), 200) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index 2020c9d68..b64cedfdf 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -415,9 +415,9 @@ class ProcessInstanceProcessor: tld.process_instance_id = process_instance_model.id # we want this to be the fully qualified path to the process model including all group subcomponents - current_app.config[ - "THREAD_LOCAL_DATA" - ].process_model_identifier = f"{process_instance_model.process_model_identifier}" + current_app.config["THREAD_LOCAL_DATA"].process_model_identifier = ( + f"{process_instance_model.process_model_identifier}" + ) self.process_instance_model = process_instance_model self.process_model_service = ProcessModelService() @@ -577,9 +577,9 @@ class ProcessInstanceProcessor: bpmn_subprocess_definition.bpmn_identifier ] = bpmn_process_definition_dict spiff_bpmn_process_dict["subprocess_specs"][bpmn_subprocess_definition.bpmn_identifier]["task_specs"] = {} - bpmn_subprocess_definition_bpmn_identifiers[ - bpmn_subprocess_definition.id - ] = bpmn_subprocess_definition.bpmn_identifier + bpmn_subprocess_definition_bpmn_identifiers[bpmn_subprocess_definition.id] = ( + bpmn_subprocess_definition.bpmn_identifier + ) task_definitions = TaskDefinitionModel.query.filter( TaskDefinitionModel.bpmn_process_definition_id.in_( # type: ignore diff --git a/spiffworkflow-frontend/.eslintrc.js b/spiffworkflow-frontend/.eslintrc.js index 75ad6cbc3..a7a64af3a 100644 --- a/spiffworkflow-frontend/.eslintrc.js +++ b/spiffworkflow-frontend/.eslintrc.js @@ -23,7 +23,7 @@ module.exports = { ecmaVersion: 'latest', sourceType: 'module', }, - plugins: ['react', 'sonarjs', '@typescript-eslint'], + plugins: ['react', 'sonarjs', '@typescript-eslint', 'unused-imports'], rules: { // according to https://github.com/typescript-eslint/typescript-eslint/issues/2621, You should turn off the eslint core rule and turn on the typescript-eslint rule // but not sure which of the above "extends" statements is maybe bringing in eslint core @@ -43,6 +43,16 @@ module.exports = { 'react/require-default-props': 'off', 'import/prefer-default-export': 'off', 'no-unused-vars': 'off', + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': [ + 'warn', + { + vars: 'all', + varsIgnorePattern: '^_', + args: 'after-used', + argsIgnorePattern: '^_', + }, + ], '@typescript-eslint/no-unused-vars': [ 'error', { diff --git a/spiffworkflow-frontend/package-lock.json b/spiffworkflow-frontend/package-lock.json index 9642f5f91..c2900d302 100644 --- a/spiffworkflow-frontend/package-lock.json +++ b/spiffworkflow-frontend/package-lock.json @@ -85,6 +85,7 @@ "eslint-plugin-react": "^7.31.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-sonarjs": "^0.15.0", + "eslint-plugin-unused-imports": "^2.0.0", "prettier": "^2.7.1", "safe-regex": "^2.1.1", "ts-migrate": "^0.1.30" @@ -13286,6 +13287,36 @@ "eslint": "^7.5.0 || ^8.0.0" } }, + "node_modules/eslint-plugin-unused-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz", + "integrity": "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==", + "dev": true, + "dependencies": { + "eslint-rule-composer": "^0.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "eslint": "^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -42460,6 +42491,21 @@ "@typescript-eslint/utils": "^5.58.0" } }, + "eslint-plugin-unused-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz", + "integrity": "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==", + "dev": true, + "requires": { + "eslint-rule-composer": "^0.3.0" + } + }, + "eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", diff --git a/spiffworkflow-frontend/package.json b/spiffworkflow-frontend/package.json index 85e00d59a..5b85e25cf 100644 --- a/spiffworkflow-frontend/package.json +++ b/spiffworkflow-frontend/package.json @@ -113,6 +113,7 @@ "eslint-plugin-react": "^7.31.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-sonarjs": "^0.15.0", + "eslint-plugin-unused-imports": "^2.0.0", "prettier": "^2.7.1", "safe-regex": "^2.1.1", "ts-migrate": "^0.1.30" diff --git a/spiffworkflow-frontend/src/App.tsx b/spiffworkflow-frontend/src/App.tsx index 8031802c0..b7eaca800 100644 --- a/spiffworkflow-frontend/src/App.tsx +++ b/spiffworkflow-frontend/src/App.tsx @@ -6,6 +6,7 @@ import { defineAbility } from '@casl/ability'; import NavigationBar from './components/NavigationBar'; import HomePageRoutes from './routes/HomePageRoutes'; +import About from './routes/About'; import ErrorBoundary from './components/ErrorBoundary'; import AdminRoutes from './routes/AdminRoutes'; import ProcessRoutes from './routes/ProcessRoutes'; @@ -35,6 +36,7 @@ export default function App() { } /> + } /> } /> } /> } /> diff --git a/spiffworkflow-frontend/src/components/NavigationBar.tsx b/spiffworkflow-frontend/src/components/NavigationBar.tsx index b00bee848..54064dc17 100644 --- a/spiffworkflow-frontend/src/components/NavigationBar.tsx +++ b/spiffworkflow-frontend/src/components/NavigationBar.tsx @@ -30,6 +30,7 @@ import { PermissionsToCheck } from '../interfaces'; import { usePermissionFetcher } from '../hooks/PermissionService'; import { UnauthenticatedError } from '../services/HttpService'; import { SPIFF_ENVIRONMENT } from '../config'; +import appVersionInfo from '../helpers/appVersionInfo'; // for ref: https://react-bootstrap.github.io/components/navbar/ export default function NavigationBar() { @@ -57,6 +58,15 @@ export default function NavigationBar() { }; const { ability } = usePermissionFetcher(permissionRequestData); + // default to readthedocs and let someone specify an environment variable to override: + // + let documentationUrl = 'https://spiffworkflow.readthedocs.io'; + if ('DOCUMENTATION_URL' in window.spiffworkflowFrontendJsenv) { + documentationUrl = window.spiffworkflowFrontendJsenv.DOCUMENTATION_URL; + } + + const versionInfo = appVersionInfo(); + useEffect(() => { let newActiveKey = '/admin/process-groups'; if (location.pathname.match(/^\/admin\/messages\b/)) { @@ -81,6 +91,12 @@ export default function NavigationBar() { return activeKey === menuItemPath; }; + let aboutLinkElement = null; + + if (Object.keys(versionInfo).length) { + aboutLinkElement = About; + } + const profileToggletip = (
@@ -99,6 +115,11 @@ export default function NavigationBar() {

{UserService.getUserEmail()}


+ {aboutLinkElement} + + Documentation + +