Merge pull request #258 from sartography/feature/version-and-documentation-links

mostly frontend stuff: add version and doc links, autofix unused imports
This commit is contained in:
Kevin Burnett 2023-05-12 21:56:11 +00:00 committed by GitHub
commit 7a0958538f
15 changed files with 220 additions and 20 deletions

View File

@ -185,13 +185,19 @@ def create_app() -> flask.app.Flask:
return app # type: ignore 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: def _setup_prometheus_metrics(app: flask.app.Flask, connexion_app: connexion.apps.flask_app.FlaskApp) -> None:
metrics = ConnexionPrometheusMetrics(connexion_app) metrics = ConnexionPrometheusMetrics(connexion_app)
app.config["PROMETHEUS_METRICS"] = metrics app.config["PROMETHEUS_METRICS"] = metrics
if os.path.isfile("version_info.json"): version_info_data = get_version_info_data()
version_info_data = {} if len(version_info_data) > 0:
with open("version_info.json") as f:
version_info_data = json.load(f)
# prometheus does not allow periods in key names # prometheus does not allow periods in key names
version_info_data_normalized = {k.replace(".", "_"): v for k, v in version_info_data.items()} 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) metrics.info("version_info", "Application Version Info", **version_info_data_normalized)

View File

@ -162,6 +162,19 @@ paths:
schema: schema:
$ref: "#/components/schemas/OkTrue" $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}: /active-users/updates/{last_visited_identifier}:
parameters: parameters:

View File

@ -18,13 +18,13 @@ def setup_database_uri(app: Flask) -> None:
if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_URI") is None: if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_URI") is None:
database_name = f"spiffworkflow_backend_{app.config['ENV_IDENTIFIER']}" database_name = f"spiffworkflow_backend_{app.config['ENV_IDENTIFIER']}"
if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "sqlite": if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "sqlite":
app.config[ app.config["SQLALCHEMY_DATABASE_URI"] = (
"SQLALCHEMY_DATABASE_URI" f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3"
] = f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3" )
elif app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "postgres": elif app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "postgres":
app.config[ app.config["SQLALCHEMY_DATABASE_URI"] = (
"SQLALCHEMY_DATABASE_URI" f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}"
] = f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}" )
else: else:
# use pswd to trick flake8 with hardcoded passwords # use pswd to trick flake8 with hardcoded passwords
db_pswd = app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_PASSWORD") db_pswd = app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_PASSWORD")

View File

@ -129,9 +129,9 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
def serialized_with_metadata(self) -> dict[str, Any]: def serialized_with_metadata(self) -> dict[str, Any]:
process_instance_attributes = self.serialized process_instance_attributes = self.serialized
process_instance_attributes["process_metadata"] = self.process_metadata process_instance_attributes["process_metadata"] = self.process_metadata
process_instance_attributes[ process_instance_attributes["process_model_with_diagram_identifier"] = (
"process_model_with_diagram_identifier" self.process_model_with_diagram_identifier
] = self.process_model_with_diagram_identifier )
return process_instance_attributes return process_instance_attributes
@property @property

View File

@ -1,6 +1,13 @@
"""APIs for dealing with process groups, process models, and process instances.""" """APIs for dealing with process groups, process models, and process instances."""
from flask import make_response
from flask.wrappers import Response from flask.wrappers import Response
from spiffworkflow_backend import get_version_info_data
def test_raise_error() -> Response: def test_raise_error() -> Response:
raise Exception("This exception was generated by /debug/test-raise-error for testing purposes. Please ignore.") 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)

View File

@ -415,9 +415,9 @@ class ProcessInstanceProcessor:
tld.process_instance_id = process_instance_model.id 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 # we want this to be the fully qualified path to the process model including all group subcomponents
current_app.config[ current_app.config["THREAD_LOCAL_DATA"].process_model_identifier = (
"THREAD_LOCAL_DATA" f"{process_instance_model.process_model_identifier}"
].process_model_identifier = f"{process_instance_model.process_model_identifier}" )
self.process_instance_model = process_instance_model self.process_instance_model = process_instance_model
self.process_model_service = ProcessModelService() self.process_model_service = ProcessModelService()
@ -577,9 +577,9 @@ class ProcessInstanceProcessor:
bpmn_subprocess_definition.bpmn_identifier bpmn_subprocess_definition.bpmn_identifier
] = bpmn_process_definition_dict ] = bpmn_process_definition_dict
spiff_bpmn_process_dict["subprocess_specs"][bpmn_subprocess_definition.bpmn_identifier]["task_specs"] = {} spiff_bpmn_process_dict["subprocess_specs"][bpmn_subprocess_definition.bpmn_identifier]["task_specs"] = {}
bpmn_subprocess_definition_bpmn_identifiers[ bpmn_subprocess_definition_bpmn_identifiers[bpmn_subprocess_definition.id] = (
bpmn_subprocess_definition.id bpmn_subprocess_definition.bpmn_identifier
] = bpmn_subprocess_definition.bpmn_identifier )
task_definitions = TaskDefinitionModel.query.filter( task_definitions = TaskDefinitionModel.query.filter(
TaskDefinitionModel.bpmn_process_definition_id.in_( # type: ignore TaskDefinitionModel.bpmn_process_definition_id.in_( # type: ignore

View File

@ -23,7 +23,7 @@ module.exports = {
ecmaVersion: 'latest', ecmaVersion: 'latest',
sourceType: 'module', sourceType: 'module',
}, },
plugins: ['react', 'sonarjs', '@typescript-eslint'], plugins: ['react', 'sonarjs', '@typescript-eslint', 'unused-imports'],
rules: { 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 // 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 // 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', 'react/require-default-props': 'off',
'import/prefer-default-export': 'off', 'import/prefer-default-export': 'off',
'no-unused-vars': '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': [ '@typescript-eslint/no-unused-vars': [
'error', 'error',
{ {

View File

@ -85,6 +85,7 @@
"eslint-plugin-react": "^7.31.0", "eslint-plugin-react": "^7.31.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-sonarjs": "^0.15.0", "eslint-plugin-sonarjs": "^0.15.0",
"eslint-plugin-unused-imports": "^2.0.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"safe-regex": "^2.1.1", "safe-regex": "^2.1.1",
"ts-migrate": "^0.1.30" "ts-migrate": "^0.1.30"
@ -13286,6 +13287,36 @@
"eslint": "^7.5.0 || ^8.0.0" "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": { "node_modules/eslint-scope": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
@ -42460,6 +42491,21 @@
"@typescript-eslint/utils": "^5.58.0" "@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": { "eslint-scope": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",

View File

@ -113,6 +113,7 @@
"eslint-plugin-react": "^7.31.0", "eslint-plugin-react": "^7.31.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-sonarjs": "^0.15.0", "eslint-plugin-sonarjs": "^0.15.0",
"eslint-plugin-unused-imports": "^2.0.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"safe-regex": "^2.1.1", "safe-regex": "^2.1.1",
"ts-migrate": "^0.1.30" "ts-migrate": "^0.1.30"

View File

@ -6,6 +6,7 @@ import { defineAbility } from '@casl/ability';
import NavigationBar from './components/NavigationBar'; import NavigationBar from './components/NavigationBar';
import HomePageRoutes from './routes/HomePageRoutes'; import HomePageRoutes from './routes/HomePageRoutes';
import About from './routes/About';
import ErrorBoundary from './components/ErrorBoundary'; import ErrorBoundary from './components/ErrorBoundary';
import AdminRoutes from './routes/AdminRoutes'; import AdminRoutes from './routes/AdminRoutes';
import ProcessRoutes from './routes/ProcessRoutes'; import ProcessRoutes from './routes/ProcessRoutes';
@ -35,6 +36,7 @@ export default function App() {
<ErrorBoundary> <ErrorBoundary>
<Routes> <Routes>
<Route path="/*" element={<HomePageRoutes />} /> <Route path="/*" element={<HomePageRoutes />} />
<Route path="/about" element={<About />} />
<Route path="/tasks/*" element={<HomePageRoutes />} /> <Route path="/tasks/*" element={<HomePageRoutes />} />
<Route path="/process/*" element={<ProcessRoutes />} /> <Route path="/process/*" element={<ProcessRoutes />} />
<Route path="/admin/*" element={<AdminRoutes />} /> <Route path="/admin/*" element={<AdminRoutes />} />

View File

@ -30,6 +30,7 @@ import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService'; import { usePermissionFetcher } from '../hooks/PermissionService';
import { UnauthenticatedError } from '../services/HttpService'; import { UnauthenticatedError } from '../services/HttpService';
import { SPIFF_ENVIRONMENT } from '../config'; import { SPIFF_ENVIRONMENT } from '../config';
import appVersionInfo from '../helpers/appVersionInfo';
// for ref: https://react-bootstrap.github.io/components/navbar/ // for ref: https://react-bootstrap.github.io/components/navbar/
export default function NavigationBar() { export default function NavigationBar() {
@ -57,6 +58,15 @@ export default function NavigationBar() {
}; };
const { ability } = usePermissionFetcher(permissionRequestData); 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(() => { useEffect(() => {
let newActiveKey = '/admin/process-groups'; let newActiveKey = '/admin/process-groups';
if (location.pathname.match(/^\/admin\/messages\b/)) { if (location.pathname.match(/^\/admin\/messages\b/)) {
@ -81,6 +91,12 @@ export default function NavigationBar() {
return activeKey === menuItemPath; return activeKey === menuItemPath;
}; };
let aboutLinkElement = null;
if (Object.keys(versionInfo).length) {
aboutLinkElement = <a href="/about">About</a>;
}
const profileToggletip = ( const profileToggletip = (
<div style={{ display: 'flex' }} id="user-profile-toggletip"> <div style={{ display: 'flex' }} id="user-profile-toggletip">
<Toggletip isTabTip align="bottom-right"> <Toggletip isTabTip align="bottom-right">
@ -99,6 +115,11 @@ export default function NavigationBar() {
</p> </p>
<p>{UserService.getUserEmail()}</p> <p>{UserService.getUserEmail()}</p>
<hr /> <hr />
{aboutLinkElement}
<a target="_blank" href={documentationUrl} rel="noreferrer">
Documentation
</a>
<hr />
<Button <Button
data-qa="logout-button" data-qa="logout-button"
className="button-link" className="button-link"

View File

@ -0,0 +1,25 @@
const appVersionInfo = () => {
const versionInfoFromHtmlMetaTag = document.querySelector(
'meta[name="version-info"]'
);
let versionInfo: { [key: string]: string } = {};
if (versionInfoFromHtmlMetaTag) {
const versionInfoContentString =
versionInfoFromHtmlMetaTag.getAttribute('content');
if (
versionInfoContentString &&
versionInfoContentString !== '%REACT_APP_VERSION_INFO%'
) {
versionInfo = JSON.parse(versionInfoContentString);
}
}
versionInfo = {
version: '1.0.0',
git_sha: 'sdkfjksd',
sure: '3',
};
return versionInfo;
};
export default appVersionInfo;

View File

@ -562,3 +562,7 @@ svg.notification-icon {
.user-circle:nth-child(n+11) { .user-circle:nth-child(n+11) {
background-color: #8e8e8e; background-color: #8e8e8e;
} }
.version-info-column {
width: 50%;
}

View File

@ -111,6 +111,8 @@ export interface ProcessReference {
is_primary: boolean; is_primary: boolean;
} }
export type ObjectWithStringKeysAndValues = { [key: string]: string };
export interface ProcessFile { export interface ProcessFile {
content_type: string; content_type: string;
last_modified: string; last_modified: string;

View File

@ -0,0 +1,63 @@
// @ts-ignore
import { Table } from '@carbon/react';
import { useEffect, useState } from 'react';
import appVersionInfo from '../helpers/appVersionInfo';
import { ObjectWithStringKeysAndValues } from '../interfaces';
import HttpService from '../services/HttpService';
export default function About() {
const frontendVersionInfo = appVersionInfo();
const [backendVersionInfo, setBackendVersionInfo] =
useState<ObjectWithStringKeysAndValues | null>(null);
useEffect(() => {
const handleVersionInfoResponse = (
response: ObjectWithStringKeysAndValues
) => {
setBackendVersionInfo(response);
};
HttpService.makeCallToBackend({
path: `/debug/version-info`,
successCallback: handleVersionInfoResponse,
});
}, []);
const versionInfoFromDict = (
title: string,
versionInfoDict: ObjectWithStringKeysAndValues | null
) => {
if (versionInfoDict !== null && Object.keys(versionInfoDict).length) {
const tableRows = Object.keys(versionInfoDict).map((key) => {
const value = versionInfoDict[key];
return (
<tr key={key}>
<td className="version-info-column">
<strong>{key}</strong>
</td>
<td className="version-info-column">{value}</td>
</tr>
);
});
return (
<>
<h2 title="This information is configurable by specifying values in version_info.json in the app at build time">
{title}
</h2>
<Table striped bordered>
<tbody>{tableRows}</tbody>
</Table>
</>
);
}
return null;
};
return (
<div>
<h1>About</h1>
{versionInfoFromDict('Frontend version information', frontendVersionInfo)}
{versionInfoFromDict('Backend version information', backendVersionInfo)}
</div>
);
}