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:
commit
7a0958538f
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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() {
|
|||
<ErrorBoundary>
|
||||
<Routes>
|
||||
<Route path="/*" element={<HomePageRoutes />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/tasks/*" element={<HomePageRoutes />} />
|
||||
<Route path="/process/*" element={<ProcessRoutes />} />
|
||||
<Route path="/admin/*" element={<AdminRoutes />} />
|
||||
|
|
|
@ -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 = <a href="/about">About</a>;
|
||||
}
|
||||
|
||||
const profileToggletip = (
|
||||
<div style={{ display: 'flex' }} id="user-profile-toggletip">
|
||||
<Toggletip isTabTip align="bottom-right">
|
||||
|
@ -99,6 +115,11 @@ export default function NavigationBar() {
|
|||
</p>
|
||||
<p>{UserService.getUserEmail()}</p>
|
||||
<hr />
|
||||
{aboutLinkElement}
|
||||
<a target="_blank" href={documentationUrl} rel="noreferrer">
|
||||
Documentation
|
||||
</a>
|
||||
<hr />
|
||||
<Button
|
||||
data-qa="logout-button"
|
||||
className="button-link"
|
||||
|
|
|
@ -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;
|
|
@ -562,3 +562,7 @@ svg.notification-icon {
|
|||
.user-circle:nth-child(n+11) {
|
||||
background-color: #8e8e8e;
|
||||
}
|
||||
|
||||
.version-info-column {
|
||||
width: 50%;
|
||||
}
|
||||
|
|
|
@ -111,6 +111,8 @@ export interface ProcessReference {
|
|||
is_primary: boolean;
|
||||
}
|
||||
|
||||
export type ObjectWithStringKeysAndValues = { [key: string]: string };
|
||||
|
||||
export interface ProcessFile {
|
||||
content_type: string;
|
||||
last_modified: string;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue