diff --git a/.github/workflows/backend_tests.yml b/.github/workflows/backend_tests.yml index 9e7cb821..9d36ebc3 100644 --- a/.github/workflows/backend_tests.yml +++ b/.github/workflows/backend_tests.yml @@ -50,12 +50,15 @@ jobs: session: "tests", database: "sqlite", } - - { - python: "3.10", - os: "windows-latest", - session: "tests", - database: "sqlite", - } + # FIXME: tests cannot pass on windows and we currently cannot debug + # since none of us have a windows box that can run the python app. + # so ignore windows tests until we can get it fixed. + # - { + # python: "3.10", + # os: "windows-latest", + # session: "tests", + # database: "sqlite", + # } - { python: "3.11", os: "macos-latest", diff --git a/.github/workflows/docker_image_for_main_builds.yml b/.github/workflows/docker_image_for_main_builds.yml index f8a1162e..f66a8441 100644 --- a/.github/workflows/docker_image_for_main_builds.yml +++ b/.github/workflows/docker_image_for_main_builds.yml @@ -59,6 +59,8 @@ jobs: uses: docker/metadata-action@v4.4.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + labels: | + org.opencontainers.image.description=Frontend component of SpiffWorkflow, a software development platform for building, running, and monitoring executable diagrams tags: | type=ref,event=branch,suffix=-latest type=ref,event=branch,suffix=-${{ steps.date.outputs.date }} @@ -103,6 +105,8 @@ jobs: uses: docker/metadata-action@v4.4.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + labels: | + org.opencontainers.image.description=Backend component of SpiffWorkflow, a software development platform for building, running, and monitoring executable diagrams tags: | type=ref,event=branch,suffix=-latest type=ref,event=branch,suffix=-${{ steps.date.outputs.date }} diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..a83fae56 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,29 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + # You can also specify other tool versions: + # nodejs: "19" + # rust: "1.64" + # golang: "1.19" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# If using Sphinx, optionally build your docs in additional formats such as PDF +# formats: +# - pdf + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: docs/requirements.txt \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index cc4df545..c5e01c28 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -49,20 +49,23 @@ pipeline { stages { stage('Prep') { steps { script { - def jobMetaJson = new JsonBuilder([ - git_commit: env.GIT_COMMIT.take(7), - git_branch: env.GIT_BRANCH, - build_id: env.BUILD_ID, - ]).toPrettyString() - sh "echo '${jobMetaJson}' > version_info.json" + dir("spiffworkflow-${params.COMPONENT}") { + def jobMetaJson = new JsonBuilder([ + git_commit: env.GIT_COMMIT.take(7), + git_branch: env.GIT_BRANCH, + build_id: env.BUILD_ID, + ]).toPrettyString() + sh "echo '${jobMetaJson}' > version_info.json" + } } } } stage('Build') { steps { script { dir("spiffworkflow-${params.COMPONENT}") { + /* Tag and Commit is combined to avoid clashes of parallel builds. */ image = docker.build( - "${params.DOCKER_NAME}:${env.GIT_COMMIT.take(8)}", + "${params.DOCKER_NAME}:${params.DOCKER_TAG}-${env.GIT_COMMIT.take(8)}", "--label=commit='${env.GIT_COMMIT.take(8)}' ." ) } diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..86465dfb --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +_build +.venv +.vscode \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d4bb2cbb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..ff823f2a --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,35 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'SpiffWorkflow' +copyright = '2023, Sartography' +author = 'Sartography' # Very ok to add your name here. +release = '0.1' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ['myst_parser'] + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '.venv'] + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +#html_theme = 'alabaster' +html_theme = "sphinx_rtd_theme" +html_static_path = ['static'] +html_logo = "spiffworkflow_logo.png" +html_theme_options = { + 'logo_only': True, + 'display_version': False, +} +html_css_files = ["custom.css"] \ No newline at end of file diff --git a/docs/documentation/documentation.md b/docs/documentation/documentation.md new file mode 100644 index 00000000..ccef5301 --- /dev/null +++ b/docs/documentation/documentation.md @@ -0,0 +1,131 @@ +# How to Contribute to the Documentation + +This documentation is currently hosted live at [Spiff-Arena's ReadTheDocs](https://spiff-arena.readthedocs.io/en/latest/) + + +Please set aside a couple of hours to work through this, as getting this setup correctly once is 10,000 times better than having problems every day for the rest of your life. + +## Our Methodology + +The methodology we are following is knowns as ["Docs as Code"](https://www.writethedocs.org/guide/docs-as-code/) + +This means using the same tools and processes that software developers use for writing code to write the documenation for code. +In following this methodoloy, you will have to pick up some tools you haven't had to use before (Git, Sphinx). +Why would a technical writer need to learn these software engineering tools? +I'll never make the case as well as an article by [Tom Johnson](https://idratherbewriting.com/trends/trends-to-follow-or-forget-docs-as-code.html). + +You might notice, when looking at the markdown files, that every sentence starts on a new line. +Like this one. +Unless there is a blank line between sentences, Markdown will still render this as a paragraph. +This is called [Ventilated Code](https://vanemden.wordpress.com/2009/01/01/ventilated-prose/) and can be very helpful when working in Markdown. + + +## Our Tools + +[Markdown](https://www.markdownguide.org/getting-started/) is a "markup language that you can use to add formatting elements to plain text documents. +You won't be writing the documentation in a word processor, but in simple plain text, and using some special syntax that will consistently and professionally format that text. + + +The basic Markdown syntax is very simple. Here are some [quick examples](https://commonmark.org/help/). And here is a great [10 minute tutorial](https://commonmark.org/help/tutorial/). +This will cover a lot of the basics, like bolding text, italics, paragraphs, lists and other common formatting techniques. + +![Markdown screenshot](./images/markdown.png "Markdown example") + +### MyST +Markdown doesn't support some really useful things. +You can't add footnotes, or create an "aside" comment or build a table. +Because of this there are many extensions typically referened to as Markdown "Flavors". +The flavor we are using is MyST. +There is [excellent documentation on MyST](https://myst-parser.readthedocs.io/en/v0.13.5/using/syntax.html) that you should definitely review, so you know everthing that is available to you. + + +### Sphinx +This is a large documenation effort. Many different Markdown pages will together make up the full website. + +You will mostly use Sphinx in the background - you won't be aware of it. +But if you decide that you want to alter the theme (the colors, styles, etc...) of the final website, Sphinx controls this and offers [themes](https://sphinx-themes.org/) and the ability to change styles / colors and formatting through the site. +You just need to learn a little CSS to control it. + + +### GitHub +Our project is managed by a version control system called Git. +You can use GIT to submit changes to the documenation, in the same we use to submit changes to our code. +It is avilable on GitHub as the [spiff-arena project](https://github.com/sartography/spiff-arena). Git also manages versions of the code, and handles running tests, and causing our documenation to be built and deployed. +It will take a bit to get comfortable with Git, but when you do, you will come to love it (or maybe hate it, but with a lot of respect) + +## Setup + +So that's a lot of tools, and seemingly a lot to learn. +But you will find that most of it just works - and that once you get into a regular flow, it will become second nature. + + +### Step 1: Pre-Requisites +Assure you have been granted write access to our repository. +Make sure you have an account on GitHub and then contact dan@sartography.com and ask him to add you as a contributor. + + +### Step 2: Install VSCode +[Download VSCode](https://code.visualstudio.com/) and install it on your computer. + +### Step 3: Install Python +We need python in order to build the website locally so we can really see what our content is going to look like once we publish. It's going to be handy for other reasons as well. We'll want python to be properly set up inside of VS Code. Follow [these directions and brief tutorial](https://code.visualstudio.com/docs/python/python-tutorial +) to assure this is set up. + + + +### Step 3: Connect VSCode to Git +VSCode comes with Git built in. +So you can use VSCode to "pull" changes from others down to your local computer and "push" changes back up to share with others (and to cause our docs site to rebuild) + +Here are directions for how to [clone Spiff-Arena](https://learn.microsoft.com/en-us/azure/developer/javascript/how-to/with-visual-studio-code/clone-github-repository?tabs=create-repo-command-palette%2Cinitialize-repo-activity-bar%2Ccreate-branch-command-palette%2Ccommit-changes-command-palette%2Cpush-command-palette#clone-repository). **IMPORTANT**: Follow those directions, but be sure to checkout https://github.com/sartography/spiff-arena instead of the project they are using! + +You can save the project to any directory on your computer. +We strongly suggest you create a sub-folder called "projects" in your "home" or "Desktop" folder and checkout the code into this directory. + +### Step 4: Open just the Docs Folder + +We've checked out the whole spiff-arena project, but we are only going to be working inside of the docs directory. So let's open just that folder in VSCode. + +* Go to File -> Open Folder +* Select the "docs" folder inside of spiff-arena. + +Now clikc on the two pieces of paper at the top corner of your screen, and you should see a project that looks like this: + +![Docs Directory](./images/docs_dir.png "Docs Directory") + +Without all the rest of the code in your way. + +### Step 4: Add some extensions + * Inside VSCode, go to File -> Preferences -> Extensions + * Search for "myst" + * click the "install" button. + * Repeat, this time doing it for "python extension for VS Code" + +![Myst Extension](./images/myst.png "Search or MyST in extensions") + + +### Step 5: Install Python Dependencies +This project requires a few Python dependencies to work correctly. We are going to set up a Virtual Evironment for Python to keep us sane later on. You can do that by following these steps: + +1. Open the Command Palette (Ctrl+Shift+P), start typing the **Python: Create Environment** command to search, and then select the command. +1. Select **Venv** +1. Select Python 3.11 from the list of options if there is nore than one thing to select. +1. Be sure the the checkbox next to "requirements.txt" is selected. +1. Click OK. + +### Step 6: Fire up the website +1. Go to Terminial -> New Terminal +1. type: **sphinx-autobuild . _build/html** at the prompt and hit enter. +1. Open your browser and go to http://127.0.0.1:8000 + + +### Step 7: Make a chance +1. Open up a markdown file, and make a change. + +### Step 8: Commit your changes and push them up for everyone. +1. Select the "git" button on the left hand side of the toolbar (cricles with lines between them) ![Git button](./images/git.png "Git button") + +2. Press the blue "Commit" button. + +3. Any changes you pushed up, should be live on our website within 5 to 10 minutes. + diff --git a/docs/documentation/images/commit.webp b/docs/documentation/images/commit.webp new file mode 100644 index 00000000..20343768 Binary files /dev/null and b/docs/documentation/images/commit.webp differ diff --git a/docs/documentation/images/docs_dir.png b/docs/documentation/images/docs_dir.png new file mode 100644 index 00000000..c2e36fa1 Binary files /dev/null and b/docs/documentation/images/docs_dir.png differ diff --git a/docs/documentation/images/git.png b/docs/documentation/images/git.png new file mode 100644 index 00000000..7845d81b Binary files /dev/null and b/docs/documentation/images/git.png differ diff --git a/docs/documentation/images/markdown.png b/docs/documentation/images/markdown.png new file mode 100644 index 00000000..91c13e03 Binary files /dev/null and b/docs/documentation/images/markdown.png differ diff --git a/docs/documentation/images/myst.png b/docs/documentation/images/myst.png new file mode 100644 index 00000000..919ad53f Binary files /dev/null and b/docs/documentation/images/myst.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..c5442c2e --- /dev/null +++ b/docs/index.md @@ -0,0 +1,18 @@ +Welcome to SpiffWorkflow's documentation! +======================================= + +```{toctree} +:maxdepth: 2 +:caption: Contents +quick_start/quick_start.md +documentation/documentation.md +``` + +This is great! + +Indices and tables +================== + +* [](genindex) +* [](modindex) +* [](search) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..954237b9 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/quick_start/images/Untitled.png b/docs/quick_start/images/Untitled.png new file mode 100644 index 00000000..95c7a4ff Binary files /dev/null and b/docs/quick_start/images/Untitled.png differ diff --git a/docs/quick_start/images/Untitled_1.png b/docs/quick_start/images/Untitled_1.png new file mode 100644 index 00000000..8289df4b Binary files /dev/null and b/docs/quick_start/images/Untitled_1.png differ diff --git a/docs/quick_start/images/Untitled_10.png b/docs/quick_start/images/Untitled_10.png new file mode 100644 index 00000000..20aae62c Binary files /dev/null and b/docs/quick_start/images/Untitled_10.png differ diff --git a/docs/quick_start/images/Untitled_11.png b/docs/quick_start/images/Untitled_11.png new file mode 100644 index 00000000..4d7167c1 Binary files /dev/null and b/docs/quick_start/images/Untitled_11.png differ diff --git a/docs/quick_start/images/Untitled_12.png b/docs/quick_start/images/Untitled_12.png new file mode 100644 index 00000000..42937266 Binary files /dev/null and b/docs/quick_start/images/Untitled_12.png differ diff --git a/docs/quick_start/images/Untitled_13.png b/docs/quick_start/images/Untitled_13.png new file mode 100644 index 00000000..b0c07d7e Binary files /dev/null and b/docs/quick_start/images/Untitled_13.png differ diff --git a/docs/quick_start/images/Untitled_14.png b/docs/quick_start/images/Untitled_14.png new file mode 100644 index 00000000..4d7167c1 Binary files /dev/null and b/docs/quick_start/images/Untitled_14.png differ diff --git a/docs/quick_start/images/Untitled_15.png b/docs/quick_start/images/Untitled_15.png new file mode 100644 index 00000000..08f77d6a Binary files /dev/null and b/docs/quick_start/images/Untitled_15.png differ diff --git a/docs/quick_start/images/Untitled_16.png b/docs/quick_start/images/Untitled_16.png new file mode 100644 index 00000000..24063a12 Binary files /dev/null and b/docs/quick_start/images/Untitled_16.png differ diff --git a/docs/quick_start/images/Untitled_17.png b/docs/quick_start/images/Untitled_17.png new file mode 100644 index 00000000..5f87b747 Binary files /dev/null and b/docs/quick_start/images/Untitled_17.png differ diff --git a/docs/quick_start/images/Untitled_18.png b/docs/quick_start/images/Untitled_18.png new file mode 100644 index 00000000..eec05dc8 Binary files /dev/null and b/docs/quick_start/images/Untitled_18.png differ diff --git a/docs/quick_start/images/Untitled_19.png b/docs/quick_start/images/Untitled_19.png new file mode 100644 index 00000000..c79620c8 Binary files /dev/null and b/docs/quick_start/images/Untitled_19.png differ diff --git a/docs/quick_start/images/Untitled_2.png b/docs/quick_start/images/Untitled_2.png new file mode 100644 index 00000000..fb3ba47d Binary files /dev/null and b/docs/quick_start/images/Untitled_2.png differ diff --git a/docs/quick_start/images/Untitled_20.png b/docs/quick_start/images/Untitled_20.png new file mode 100644 index 00000000..8b893f7b Binary files /dev/null and b/docs/quick_start/images/Untitled_20.png differ diff --git a/docs/quick_start/images/Untitled_21.png b/docs/quick_start/images/Untitled_21.png new file mode 100644 index 00000000..653765ff Binary files /dev/null and b/docs/quick_start/images/Untitled_21.png differ diff --git a/docs/quick_start/images/Untitled_22.png b/docs/quick_start/images/Untitled_22.png new file mode 100644 index 00000000..8a0c335c Binary files /dev/null and b/docs/quick_start/images/Untitled_22.png differ diff --git a/docs/quick_start/images/Untitled_23.png b/docs/quick_start/images/Untitled_23.png new file mode 100644 index 00000000..1d650d5b Binary files /dev/null and b/docs/quick_start/images/Untitled_23.png differ diff --git a/docs/quick_start/images/Untitled_24.png b/docs/quick_start/images/Untitled_24.png new file mode 100644 index 00000000..039e31c4 Binary files /dev/null and b/docs/quick_start/images/Untitled_24.png differ diff --git a/docs/quick_start/images/Untitled_25.png b/docs/quick_start/images/Untitled_25.png new file mode 100644 index 00000000..f912d4cc Binary files /dev/null and b/docs/quick_start/images/Untitled_25.png differ diff --git a/docs/quick_start/images/Untitled_26.png b/docs/quick_start/images/Untitled_26.png new file mode 100644 index 00000000..e45f20b4 Binary files /dev/null and b/docs/quick_start/images/Untitled_26.png differ diff --git a/docs/quick_start/images/Untitled_27.png b/docs/quick_start/images/Untitled_27.png new file mode 100644 index 00000000..e81db979 Binary files /dev/null and b/docs/quick_start/images/Untitled_27.png differ diff --git a/docs/quick_start/images/Untitled_28.png b/docs/quick_start/images/Untitled_28.png new file mode 100644 index 00000000..93776de4 Binary files /dev/null and b/docs/quick_start/images/Untitled_28.png differ diff --git a/docs/quick_start/images/Untitled_29.png b/docs/quick_start/images/Untitled_29.png new file mode 100644 index 00000000..2f646c01 Binary files /dev/null and b/docs/quick_start/images/Untitled_29.png differ diff --git a/docs/quick_start/images/Untitled_3.png b/docs/quick_start/images/Untitled_3.png new file mode 100644 index 00000000..98aecc31 Binary files /dev/null and b/docs/quick_start/images/Untitled_3.png differ diff --git a/docs/quick_start/images/Untitled_30.png b/docs/quick_start/images/Untitled_30.png new file mode 100644 index 00000000..0cdb7474 Binary files /dev/null and b/docs/quick_start/images/Untitled_30.png differ diff --git a/docs/quick_start/images/Untitled_31.png b/docs/quick_start/images/Untitled_31.png new file mode 100644 index 00000000..b059a4ad Binary files /dev/null and b/docs/quick_start/images/Untitled_31.png differ diff --git a/docs/quick_start/images/Untitled_32.png b/docs/quick_start/images/Untitled_32.png new file mode 100644 index 00000000..2e99f7c0 Binary files /dev/null and b/docs/quick_start/images/Untitled_32.png differ diff --git a/docs/quick_start/images/Untitled_4.png b/docs/quick_start/images/Untitled_4.png new file mode 100644 index 00000000..6d827afa Binary files /dev/null and b/docs/quick_start/images/Untitled_4.png differ diff --git a/docs/quick_start/images/Untitled_5.png b/docs/quick_start/images/Untitled_5.png new file mode 100644 index 00000000..e06a5afa Binary files /dev/null and b/docs/quick_start/images/Untitled_5.png differ diff --git a/docs/quick_start/images/Untitled_6.png b/docs/quick_start/images/Untitled_6.png new file mode 100644 index 00000000..767e30c7 Binary files /dev/null and b/docs/quick_start/images/Untitled_6.png differ diff --git a/docs/quick_start/images/Untitled_7.png b/docs/quick_start/images/Untitled_7.png new file mode 100644 index 00000000..f7c600bf Binary files /dev/null and b/docs/quick_start/images/Untitled_7.png differ diff --git a/docs/quick_start/images/Untitled_8.png b/docs/quick_start/images/Untitled_8.png new file mode 100644 index 00000000..7ac61b79 Binary files /dev/null and b/docs/quick_start/images/Untitled_8.png differ diff --git a/docs/quick_start/images/Untitled_9.png b/docs/quick_start/images/Untitled_9.png new file mode 100644 index 00000000..325794d6 Binary files /dev/null and b/docs/quick_start/images/Untitled_9.png differ diff --git a/docs/quick_start/quick_start.md b/docs/quick_start/quick_start.md new file mode 100644 index 00000000..d3ef647c --- /dev/null +++ b/docs/quick_start/quick_start.md @@ -0,0 +1,306 @@ +# Quick start guide + +```{admonition} Welcome to the SpiffWorkflow quick start guide! +:class: info + +👇 Throughout this step-by-step guide, we will walk you through key components of SpiffWorkflow, ensuring that you have a clear understanding of how to use the platform effectively. +``` + + +## 🚀 Getting Started with SpiffWorkflow + +SpiffWorkflow is a platform that facilitates the execution of business processes performed within the Status platform. + +To access SpiffWorkflow, simply sign in using your Keycloak account. Once you have successfully signed in to the Spiff platform, it is crucial to familiarize yourself with the various sections within the SpiffWorkflow. This will enable you to gain a comprehensive understanding of the interface. + +```{image} images/Untitled.png +:alt: Login Page +:width: 45% +``` +```{image} images/Untitled_1.png +:alt: Home Page +:width: 45% +``` + +```{admonition} Signing In +:class: warning + +⚠️ In the event that you encounter any difficulties signing in to Spiff, please reach out to Jakub (**@jakubgs**) on Discord for assistance and further guidance. +``` + +Here, we will provide a generic overview of each section step by step, allowing you to navigate and engage with the platform more effectively. + +### Step 1: Explore the Home section + +Once you are signed in, you can start exploring the home page. The home page has three tab sections: **In Progress**, **Completed** and **Start New**.  + +![Untitled](images/Untitled_2.png) + +- The "In Progress" section provides an overview of all ongoing process instances, including those initiated by you, those awaiting your action, or those awaiting action from a team you are a member of (Optional). +- The "Completed" section allows you to view all completed process instances, including those initiated by you, those initiated by other SpiffWorkflow users with tasks completed by you and if applicable, those with tasks completed by a group of which you are a member. +- The “Start New” section displays the processes you are permitted to start according to your role. + +```{admonition} Signing In +:class: info +💡 **Process:** A process is a sequence of tasks that must be completed to achieve a specific goal. + +**Instance:** An instance, on the other hand, represents a specific occurrence of a process. Each instance has its own set of data and state that is updated as the instance progresses through the workflow. +``` + +If you are a member of a team, you may also have one or more Instances with tasks waiting for [team name] lists as well. + +![Untitled](images/Untitled_3.png) + +### Step 2: Explore the Processes section + + +The process section provides a comprehensive view of the process ecosystem by showcasing process groups and process models. + +```{admonition} Process Groups +:class: info +💡 A **process group** is a way of grouping a bunch of **process models.** A **process model** contains all the files necessary to execute a specific process. +``` +-- +![Untitled](images/Untitled_4.png) + +### Step 3: Explore the Process Instances section + +The Process Instance section provides a detailed view of individual process instances, allowing you to track their progress and manage them effectively. This section includes essential information such as the instance ID, process name, the individual who started the process, the end date, and the current status. + +![Untitled](images/Untitled_5.png) + +```{admonition} Desktop Notifications +:class: info +💡 To receive SpiffWorkflow notifications in StatusApp Desktop, Public key from your Status account should be added to your **Bamboo profile**. This will ensure that workflow-related notifications are sent to you. + +``` + +When getting started with SpiffWorkflow, it's essential to take the time to explore and familiarize yourself with the platform's interface and features. Feel free to ask questions about the platform's features or how to get started. The PPG team is always on hand to provide assistance and support when needed. + +--- + +## 🌱 How to Start a Process + +With SpiffWorkflow, you can easily initiate a new process instance. Here's a step-by-step guide on how to start a process. + +### Step 1: Sign in and navigate to Home section + +The first thing you need to do is sign in to your account on SpiffWorkflow. Once you're signed in, you'll see three tabs in the Home section: In progress, Completed, and Start New. If you want to start a new process, click the "Start New +" button. This will bring up "Processes I can start" section. + +![Untitled](images/Untitled_6.png) + +```{admonition} The Landing Page +:class: info +💡 The landing page will be the **Home section** by default, and you can navigate to other sections. +``` + +### Step 2: Select the process + +Next, you will see a list of available processes that you have permission to start. Choose the process you want to initiate and click “Start”. + +![Untitled](images/Untitled_7.png) + +Congratulations! You have successfully started a new process instance in SpiffWorkflow. + +--- + +## 🔄 How to respond to a request + +When using SpiffWorkflow, knowing how to respond to requests is essential to the process. While each request may have unique requirements, the basic steps for responding are similar. The following steps will guide you through the process of responding to requests. + +### Step 1: Navigate to the home page + +Once you are signed in, navigate to the home page of SpiffWorkflow. On the home page, you will see a list of all the requests that are available to you. + +There will be three types of instances shown: + +- **Started by me:** This section shows a list of process instances that were started by you, providing you with an overview of the instances you have initiated. +- **Waiting for me:** This section displays a list of process instances with tasks assigned to you and are currently waiting for you to respond to. +- **Waiting for [team name]:** If you are a member of SpiffWorkflow**,** this section displays a list of process instances with tasks assigned to a group you are a member of and currently waiting for someone in that group to complete them. + +![Untitled](images/Untitled_8.png) + +In the case of new users who haven't started or been part of any process or been assigned to any team, you won't be able to see any items on the home page. + +![Untitled](images/Untitled_9.png) + +### Step 2: Respond to the request + +Once you have identified the request you need to respond to, simply click on the 'Go' button in the action column to open it. Upon opening the process instance, you can respond to the request based on the requirements of that task. + +Depending on the task requirements, this may involve submitting additional information, reviewing the task or any other action item. + +![Untitled](images/Untitled_10.png) + +That's it! With these simple steps, you can efficiently review tasks in SpiffWorkflow. + +--- + +## 📑 How to view process steps for the process you just started + +After starting a process, it's important to stay informed about its progress. Even though you'll receive notifications when your attention is required, it's natural to be curious about what's happening in the background. Therefore, monitoring the process steps regularly is a great way to ensure everything is moving smoothly. + +Here's how you can view the steps of the process you just started. + +### Step 1: Navigate to the “Home” or “Process Instance” section. + +There are 2 ways of finding your process instances. + +Option 1: Once you're signed in, navigate to the home section. Here you will find a list of all the processes instances you've initiated. + +![Untitled](images/Untitled_11.png) + +Option 2: You can also view the processes you have initiated in the **"Process Instances"** section. + +![Untitled](images/Untitled_12.png) + +### Step 2: Select the process instance you want to view + +Click on the process instance you want to view. This will take you to the process instance information. Navigate to the BPMN diagram section. Here you can see the current task highlighted in **yellow**. The grey represents the path which was taken by the current process steps. + +![Untitled](images/Untitled_13.png) + +By following these steps, you can easily view the steps of the process you initiated and keep track of progress. + +--- + +## 🏷️How to view the Process-defined metadata for a process instance + +The Process-defined **metadata can provide valuable insights into its history, current status, and other important details that is specifically created and used within a particular process. With the SpiffWorkflow platform, users can easily view the metadata for a process instance. + +To check the metadata of a process instance, follow these steps. + +### Step 1: Navigate to the “Home” or “Process Instance” section. + +Once you're signed in, navigate to the home section. Here you will find a list of all the process instances you've initiated under **“Started by me”**. + +![Untitled](images/Untitled_14.png) + +### Step 2: View metadata for the selected process instance + +Click on the process instance you want to view. Upon clicking this, you will be able to view the information about the given instance. You'll find the metadata under the details option in the process instance. + +![Untitled](images/Untitled_15.png) + +By following these simple steps, you can easily view the metadata for a process instance in SpiffWorkflow. + +--- + +## 📂 How to view Process Model files + +The process model files provide great transparency into our internal business rules and processes. You can dig deep into the decision-making process and really understand how the process and organization operate. With these steps, you'll be able to access process models easily and efficiently. + +### Step 1: Head over to the process section + +Once you have successfully signed in, navigate to the process section. This section allows you to access all the process groups and process models you have access to. + +![Untitled](images/Untitled_16.png) + +> **Step 2: Find and click on the process** +> + +You can either search for a process model using the search bar or navigate through displayed processes to find the process model. + +![Untitled](images/Untitled_17.png) + +### Step 3: Access the process model files + +Once you have clicked on the process you want to view, a list of the model files that are associated with the process will appear. + +![Untitled](images/Untitled_18.png) + +By following these simple steps, you can easily view process model files in SpiffWorkflow. + +--- + +## 🔍 How to view and filter process instances + +As you work on various process instances in SpiffWorkflow, you may want to view and filter some of them. This can help you track the status of various instances and manage them more efficiently. + +Here are the steps to view and filter process instances in SpiffWorkflow. + +### Step 1: Navigate to Process Instances + +Once you are signed in, navigate to the "Process Instances" section. Within the "Process Instances" section, you'll see a list of all the instances for the processes you can access. + +![Untitled](images/Untitled_19.png) + +### Step 2: Click on Filter option + +To filter the list, click on the "Filter" option. This will expand the filter section where you will be able to provide details about the process instance. This allows you to enter various details, including the process model, start date, end date, and time. To refine your search, you can enter multiple filter parameters. + +![Untitled](images/Untitled_20.png) + +### Step 3: Apply Filters: + +Once you have entered all the relevant filter details, click on the "**Apply**" button to apply the filters. The system will then display all the process instances matching the input details. + +![Untitled](images/Untitled_21.png) + +To filter process instances by **process-defined metadata**, follow these steps: + +- Search for the specific **process** you want to filter and click on the column option to select metadata options. + +![Untitled](images/Untitled_22.png) + +- The metadata fields will be displayed in dropdown. Select the field you want to display and Click on "**Save**" to apply the changes. + +![Untitled](images/Untitled_23.png) + +- After saving the details, the newly created column will be displayed. Finally click on “**Apply“** button to reflect the changes. + +![Untitled](images/Untitled_24.png) + +### (Optional) Step 4: Save Perspectives + +If you wish to save the perspectives, click on the "**Save**" button. + +![Untitled](images/Untitled_25.png) + +A prompt will appear, allowing you to provide a name for the identifier associated with the saved filter. Enter a descriptive name for the filter identifier and “**Save”** changes. Now you can search for specific processes using Process Instance Perspectives. + +![Untitled](images/Untitled_26.png) + +![Untitled](images/Untitled_27.png) + +### (Optional) Step 5: Filter by ID + +![Untitled](images/Untitled_28.png) + +If you want to filter by ID, go to the "Find by Id" section of the page. Enter the ID and click "Submit". The system will show you the process instance with the corresponding ID. + +You can now view the process instances that you filtered for and take appropriate action based on their status. This can help you manage your workflows more efficiently and keep track of the progress of various process instances. + +--- + +## 🗳️ How to request additional permissions + +As a user, you may be required to access certain process groups or start process models in order to perform desired actions. However, you may not have the necessary access or permissions to do so. In this case, you will need to request access or additional permissions from the admins - PPG team. + +By following these steps, you can submit a request and seek the necessary permissions to perform the desired actions. + +### Step 1: Navigate & Search + +Once you are signed in, navigate to the "**Process**" section. Use the search bar or browse through the available process models until you find "**Request Access**”. Click on the process model to open it. + +![Untitled](images/Untitled_29.png) + +If you want to access the request access process from **Home** section and click on the "**Start New +**" button. This will open the "Processes I can start" section where you can find the “Request Access” process. + +![Untitled](images/Untitled_30.png) + +### Step 2: Start the Process + +Once the "**Process Request**" model is open, initiate the process by clicking on the "Start" button. + +![Untitled](images/Untitled_31.png) + +### Step 3: Provide Request Details & Submit + +A task will be presented to capture the necessary information and details for special permissions request. Find the “**Description”** text field and enter the relevant information and details about your request. + +Ensure that all required details have been included such as Process name, Process group name, and type of permissions you need. Click on the "**Submit**" button or similar action to submit your access or special permissions request. + +![Untitled](images/Untitled_32.png) + +By following these steps, you can request the special permissions needed to carry out your tasks effectively. \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..2fe93683 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,46 @@ +alabaster==0.7.13 +appdirs==1.4.4 +astroid==2.15.5 +attrs==23.1.0 +Babel==2.12.1 +cattrs==22.2.0 +certifi==2023.5.7 +charset-normalizer==3.1.0 +colorama==0.4.6 +docutils==0.18.1 +esbonio==0.16.1 +idna==3.4 +imagesize==1.4.1 +Jinja2==3.1.2 +lazy-object-proxy==1.9.0 +livereload==2.6.3 +lsprotocol==2023.0.0a1 +markdown-it-py==2.2.0 +MarkupSafe==2.1.2 +mdit-py-plugins==0.3.5 +mdurl==0.1.2 +myst-parser==1.0.0 +packaging==23.1 +pygls==1.0.2 +Pygments==2.15.1 +pyspellchecker==0.7.2 +PyYAML==6.0 +requests==2.30.0 +six==1.16.0 +snowballstemmer==2.2.0 +Sphinx==6.2.1 +sphinx-autoapi==2.1.0 +sphinx-autobuild==2021.3.14 +sphinx-rtd-theme==1.2.0 +sphinxcontrib-applehelp==1.0.4 +sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-htmlhelp==2.0.1 +sphinxcontrib-jquery==4.1 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-serializinghtml==1.1.5 +tornado==6.3.2 +typeguard==3.0.2 +Unidecode==1.3.6 +urllib3==2.0.2 +wrapt==1.15.0 diff --git a/docs/spiffworkflow_logo.png b/docs/spiffworkflow_logo.png new file mode 100644 index 00000000..1c77c4c6 Binary files /dev/null and b/docs/spiffworkflow_logo.png differ diff --git a/docs/static/custom.css b/docs/static/custom.css new file mode 100644 index 00000000..e69de29b diff --git a/spiffworkflow-backend/Dockerfile b/spiffworkflow-backend/Dockerfile index f42b8a5b..9c518ed7 100644 --- a/spiffworkflow-backend/Dockerfile +++ b/spiffworkflow-backend/Dockerfile @@ -42,7 +42,7 @@ RUN poetry install --without dev FROM deployment AS final LABEL source="https://github.com/sartography/spiff-arena" -LABEL description="Software development platform for building, running, and monitoring executable diagrams" +LABEL description="Backend component of SpiffWorkflow, a software development platform for building, running, and monitoring executable diagrams" COPY --from=setup /app /app diff --git a/spiffworkflow-backend/README.rst b/spiffworkflow-backend/README.rst index 404ed520..0f030c0d 100644 --- a/spiffworkflow-backend/README.rst +++ b/spiffworkflow-backend/README.rst @@ -16,6 +16,7 @@ Spiffworkflow Backend :alt: Black + Features -------- diff --git a/spiffworkflow-backend/bin/boot_server_in_docker b/spiffworkflow-backend/bin/boot_server_in_docker index fb425af5..96d3d79f 100755 --- a/spiffworkflow-backend/bin/boot_server_in_docker +++ b/spiffworkflow-backend/bin/boot_server_in_docker @@ -40,15 +40,16 @@ fi additional_args="" -if [[ "${SPIFFWORKFLOW_BACKEND_APPLICATION_ROOT:-}" != "/" ]]; then - additional_args="${additional_args} -e SCRIPT_NAME=${SPIFFWORKFLOW_BACKEND_APPLICATION_ROOT}" +app_root="${SPIFFWORKFLOW_BACKEND_APPLICATION_ROOT:-}" +if [[ -n "$app_root" ]] && [[ "${app_root}" != "/" ]]; then + additional_args="${additional_args} -e SCRIPT_NAME=${app_root}" fi # HACK: if loading fixtures for acceptance tests when we do not need multiple workers # it causes issues with attempting to add duplicate data to the db -workers=3 +worker_count=4 if [[ "${SPIFFWORKFLOW_BACKEND_LOAD_FIXTURE_DATA:-}" == "true" ]]; then - workers=1 + worker_count=1 fi if [[ "${SPIFFWORKFLOW_BACKEND_RUN_DATA_SETUP:-}" != "false" ]]; then @@ -67,11 +68,35 @@ fi git init "${SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR}" git config --global --add safe.directory "${SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR}" +if [[ -z "${SPIFFWORKFLOW_BACKEND_THREADS_PER_WORKER:-}" ]]; then + # default to 3 * 2 = 6 threads per worker + # you may want to configure threads_to_use_per_core based on whether your workload is more cpu intensive or more I/O intensive: + # cpu heavy, make it smaller + # I/O heavy, make it larger + threads_to_use_per_core=3 + + # https://stackoverflow.com/a/55423170/6090676 + # if we had access to python (i'm not sure i want to run another python script here), + # we could do something like this (on linux) to get the number of cores available to this process and a better estimate of a + # reasonable num_cores_multiple_for_threads + # if hasattr(os, 'sched_getaffinity') + # number_of_available_cores = os.sched_getaffinity(0) + # BUT the python solution isn't even as portable as this one, which is mostly posix compliant and works on linux/mac/freebsd. + num_cores_multiple_for_threads=$(getconf _NPROCESSORS_ONLN 2>/dev/null || getconf NPROCESSORS_ONLN 2>/dev/null || echo 1) + + SPIFFWORKFLOW_BACKEND_THREADS_PER_WORKER=$((threads_to_use_per_core * num_cores_multiple_for_threads)) + export SPIFFWORKFLOW_BACKEND_THREADS_PER_WORKER +fi + +# --worker-class is not strictly necessary, since setting threads will automatically set the worker class to gthread, but meh export IS_GUNICORN="true" # THIS MUST BE THE LAST COMMAND! exec poetry run gunicorn ${additional_args} \ --bind "0.0.0.0:$port" \ - --workers="$workers" \ + --preload \ + --worker-class "gthread" \ + --workers="$worker_count" \ + --threads "$SPIFFWORKFLOW_BACKEND_THREADS_PER_WORKER" \ --limit-request-line 8192 \ --timeout "$GUNICORN_TIMEOUT_SECONDS" \ --capture-output \ diff --git a/spiffworkflow-backend/poetry.lock b/spiffworkflow-backend/poetry.lock index 0b20dffe..e7e0ba37 100644 --- a/spiffworkflow-backend/poetry.lock +++ b/spiffworkflow-backend/poetry.lock @@ -3089,7 +3089,7 @@ lxml = "*" type = "git" url = "https://github.com/sartography/SpiffWorkflow" reference = "main" -resolved_reference = "a68dec77ebb0960dd8097341df7575a34e435501" +resolved_reference = "340e9983b5afd2e6e71df883e74f7dc20d4474dd" [[package]] name = "sqlalchemy" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py b/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py index 72355fe9..ce8a3970 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 a4373629..7dcb81ce 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 a7115745..22479110 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py @@ -13,18 +13,17 @@ class ConfigurationError(Exception): """ConfigurationError.""" -def setup_database_uri(app: Flask) -> None: - """Setup_database_uri.""" +def setup_database_configs(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") @@ -34,6 +33,27 @@ def setup_database_uri(app: Flask) -> None: else: app.config["SQLALCHEMY_DATABASE_URI"] = app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_URI") + # if pool size came in from the environment, it's a string, but we need an int + # if it didn't come in from the environment, base it on the number of threads + # note that max_overflow defaults to 10, so that will give extra buffer. + pool_size = app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_POOL_SIZE") + if pool_size is not None: + pool_size = int(pool_size) + else: + # this one doesn't come from app config and isn't documented in default.py + # because we don't want to give people the impression + # that setting it in flask python configs will work. on the contrary, it's used by a bash + # script that starts the backend, so it can only be set in the environment. + threads_per_worker_config = os.environ.get("SPIFFWORKFLOW_BACKEND_THREADS_PER_WORKER") + if threads_per_worker_config is not None: + pool_size = int(threads_per_worker_config) + else: + # this is a sqlalchemy default, if we don't have any better ideas + pool_size = 5 + + app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {} + app.config["SQLALCHEMY_ENGINE_OPTIONS"]["pool_size"] = pool_size + def load_config_file(app: Flask, env_config_module: str) -> None: """Load_config_file.""" @@ -115,7 +135,7 @@ def setup_config(app: Flask) -> None: app.config["PROCESS_UUID"] = uuid.uuid4() - setup_database_uri(app) + setup_database_configs(app) setup_logger(app) if app.config["SPIFFWORKFLOW_BACKEND_DEFAULT_USER_GROUP"] == "": diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py index c1498e86..88a34faf 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/default.py @@ -33,6 +33,10 @@ SPIFFWORKFLOW_BACKEND_BACKGROUND_SCHEDULER_USER_INPUT_REQUIRED_POLLING_INTERVAL_ default="120", ) ) + +# we only use this in one place, and it checks to see if it is None. +SPIFFWORKFLOW_BACKEND_DATABASE_POOL_SIZE = environ.get("SPIFFWORKFLOW_BACKEND_DATABASE_POOL_SIZE") + SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND = environ.get( "SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND", default="http://localhost:7001" ) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_read_only.yml b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_read_only.yml new file mode 100644 index 00000000..d201a555 --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/permissions/example_read_only.yml @@ -0,0 +1,84 @@ +default_group: everybody + +groups: + admin: + users: [admin@spiffworkflow.org] + +permissions: + admin: + groups: [admin] + users: [] + allowed_permissions: [read] + uri: /* + + tasks-crud: + groups: [admin] + users: [] + allowed_permissions: [create, update, delete] + uri: /tasks/* + + process-instances-crud: + groups: [ admin ] + users: [ ] + allowed_permissions: [create, update, delete] + uri: /process-instances/* + + suspend: + groups: [admin] + users: [] + allowed_permissions: [create] + uri: /v1.0/process-instance-suspend + + terminate: + groups: [admin] + users: [] + allowed_permissions: [create] + uri: /v1.0/process-instance-terminate + + resume: + groups: [admin] + users: [] + allowed_permissions: [create] + uri: /v1.0/process-instance-resume + + reset: + groups: [admin] + users: [] + allowed_permissions: [create] + uri: /v1.0/process-instance-reset + + users-exist: + groups: [admin] + users: [] + allowed_permissions: [create] + uri: /v1.0/users/exists/by-username + + send-event: + groups: [admin] + users: [] + allowed_permissions: [create] + uri: /v1.0/send-event/* + + task-complete: + groups: [admin] + users: [] + allowed_permissions: [create] + uri: /v1.0/task-complete/* + + messages: + groups: [admin] + users: [] + allowed_permissions: [create] + uri: /v1.0/messages/* + + secrets: + groups: [admin] + users: [] + allowed_permissions: [create, update, delete] + uri: /v1.0/secrets/* + + task-data: + groups: [admin] + users: [] + allowed_permissions: [update] + uri: /v1.0/task-data/* diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/process_instance.py index 078925e7..1668565c 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 29a81447..f9507af6 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/routes/health_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/health_controller.py index 05408a4f..2cb4092b 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/health_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/health_controller.py @@ -9,7 +9,3 @@ def status() -> Response: """Status.""" ProcessInstanceModel.query.filter().first() return make_response({"ok": True}, 200) - - -def test_raise_error() -> Response: - raise Exception("This exception was generated by /status/test-raise-error for testing purposes. Please ignore.") diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py index 0c0b2c7a..3af7a412 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py @@ -438,14 +438,6 @@ def _interstitial_stream(process_instance: ProcessInstanceModel) -> Generator[st ) yield render_data("error", api_error) return - except Exception as e: - api_error = ApiError( - error_code="engine_steps_error", - message=f"Failed to complete an automated task. Error was: {str(e)}", - status_code=400, - ) - yield render_data("error", api_error) - return processor.refresh_waiting_tasks() ready_engine_task_count = get_ready_engine_step_count(processor.bpmn_process_instance) tasks = get_reportable_tasks() diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py index 5a5bc44d..761f14a2 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/authorization_service.py @@ -577,13 +577,14 @@ class AuthorizationService: permissions_to_assign.append( PermissionToAssign(permission="read", target_uri="/process-instances/report-metadata") ) + permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/active-users/*")) + permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/debug/version-info")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/process-groups")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/process-models")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/processes")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/processes/callers")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/service-tasks")) permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/user-groups/for-current-user")) - permissions_to_assign.append(PermissionToAssign(permission="read", target_uri="/active-users/*")) permissions_to_assign.append(PermissionToAssign(permission="create", target_uri="/users/exists/by-username")) permissions_to_assign.append( PermissionToAssign(permission="read", target_uri="/process-instances/find-by-id/*") 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 2020c9d6..b64cedfd 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-backend/src/spiffworkflow_backend/services/task_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py index 135838e3..017cd59e 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/task_service.py @@ -432,7 +432,8 @@ class TaskService: for task_id, task_properties in tasks.items(): # The Root task is added to the spec by Spiff when the bpmn process is instantiated # within Spiff. We do not actually need it and it's missing from our initial - # bpmn process defintion so let's avoid using it. + # bpmn process defintion so let's avoid using it. This causes issues with the hashing + # since it is not there when we initially take the hash and save the definition. if task_properties["task_spec"] == "Root": continue diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py index f30e3bf4..b742c463 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_authorization_service.py @@ -289,6 +289,7 @@ class TestAuthorizationService(BaseTest): ) -> None: expected_permissions = [ ("/active-users/*", "read"), + ("/debug/version-info", "read"), ("/process-groups", "read"), ("/process-instances/find-by-id/*", "read"), ("/process-instances/for-me", "create"), diff --git a/spiffworkflow-frontend/.eslintrc.js b/spiffworkflow-frontend/.eslintrc.js index 75ad6cbc..a7a64af3 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/Dockerfile b/spiffworkflow-frontend/Dockerfile index 65350374..2bd02a0c 100644 --- a/spiffworkflow-frontend/Dockerfile +++ b/spiffworkflow-frontend/Dockerfile @@ -29,8 +29,7 @@ RUN ./bin/build # Final image without setup dependencies. FROM base AS final -LABEL source="https://github.com/sartography/spiff-arena" -LABEL description="Software development platform for building, running, and monitoring executable diagrams" +LABEL description="Frontend component of SpiffWorkflow, a software development platform for building, running, and monitoring executable diagrams" # WARNING: On localhost frontend assumes backend is one port lowe. ENV PORT0=7001 diff --git a/spiffworkflow-frontend/cypress/e2e/tasks.cy.js b/spiffworkflow-frontend/cypress/e2e/tasks.cy.js index ac9f9a47..fde67873 100644 --- a/spiffworkflow-frontend/cypress/e2e/tasks.cy.js +++ b/spiffworkflow-frontend/cypress/e2e/tasks.cy.js @@ -81,7 +81,7 @@ describe('tasks', () => { cy.navigateToHome(); // look for somethig to make sure the homepage has loaded - cy.contains('Instances with tasks waiting for me').should('exist'); + cy.contains('Waiting for me').should('exist'); // FIXME: this will probably need a better way to link to the proper form that we want cy.contains('Go').click(); diff --git a/spiffworkflow-frontend/cypress/pilot/NDR_PP1/consultingfees.cy.js b/spiffworkflow-frontend/cypress/pilot/NDR_PP1/consultingfees.cy.js index 150f9153..77effb2a 100644 --- a/spiffworkflow-frontend/cypress/pilot/NDR_PP1/consultingfees.cy.js +++ b/spiffworkflow-frontend/cypress/pilot/NDR_PP1/consultingfees.cy.js @@ -76,24 +76,41 @@ const submitWithUser = ( .contains(/^Submit$/) .click(); - if (expectAdditionalApprovalInfoPage) { - cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 }); + // if (expectAdditionalApprovalInfoPage) { + // cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 }); - cy.get('button') - .contains(/^Continue$/) - .click(); - } + // cy.get('button') + // .contains(/^Continue$/) + // .click(); + // } - cy.visit('/'); - - cy.location({ timeout: 60000 }).should((loc) => { - expect(loc.pathname).to.eq('/'); - }); - cy.wait(2000); + cy.get('button').contains('Return to Home', { timeout: 60000 }); cy.logout(); - cy.wait(2000); }; + //Check if the process instance is completed successfully + const checkProcessInstanceCompleted = ( + username, + password, + processInstanceId +) => { + cy.wait(2000); + cy.log('========Login with : ', username); + cy.log('========processInstanceId: ', processInstanceId); + cy.login(username, password); + + cy.wait(1000); + cy.visit('/admin/process-instances/find-by-id'); + cy.get('#process-instance-id-input').type(processInstanceId); + + cy.get('button') + .contains(/^Submit$/) + .click(); + + cy.wait(2000); + cy.get('#tag-1 > span').contains('complete'); +} + // Consulting Fees Path - Without Files describe.only('Consulting Fees Path - Without Files', () => { Cypress._.times(1, () => { @@ -237,6 +254,8 @@ describe.only('Consulting Fees Path - Without Files', () => { 'Task: Reminder: Check Existing Budget', 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -378,6 +397,8 @@ describe.only('Consulting Fees Path - Without Files', () => { null, 'reject' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -539,6 +560,8 @@ describe.only('Consulting Fees Path - Without Files', () => { 'Task: Reminder: Check Existing Budget', 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); }); @@ -734,6 +757,8 @@ describe('Consulting Fees Path - With Files', () => { 'Task: Reminder: Check Existing Budget', 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -925,6 +950,8 @@ describe('Consulting Fees Path - With Files', () => { null, 'reject' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -1131,6 +1158,8 @@ describe('Consulting Fees Path - With Files', () => { 'Task: Reminder: Check Existing Budget', 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); }); diff --git a/spiffworkflow-frontend/cypress/pilot/NDR_PP1/equipment.cy.js b/spiffworkflow-frontend/cypress/pilot/NDR_PP1/equipment.cy.js index 5095448b..2b387216 100644 --- a/spiffworkflow-frontend/cypress/pilot/NDR_PP1/equipment.cy.js +++ b/spiffworkflow-frontend/cypress/pilot/NDR_PP1/equipment.cy.js @@ -82,24 +82,41 @@ const submitWithUser = ( .contains(/^Submit$/) .click(); - if (expectAdditionalApprovalInfoPage) { - cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 }); + // if (expectAdditionalApprovalInfoPage) { + // cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 }); - cy.get('button') - .contains(/^Continue$/) - .click(); - } + // cy.get('button') + // .contains(/^Continue$/) + // .click(); + // } - cy.visit('/'); - - cy.location({ timeout: 60000 }).should((loc) => { - expect(loc.pathname).to.eq('/'); - }); - cy.wait(2000); + cy.get('button').contains('Return to Home', { timeout: 60000 }); cy.logout(); - cy.wait(2000); }; + //Check if the process instance is completed successfully + const checkProcessInstanceCompleted = ( + username, + password, + processInstanceId +) => { + cy.wait(2000); + cy.log('========Login with : ', username); + cy.log('========processInstanceId: ', processInstanceId); + cy.login(username, password); + + cy.wait(1000); + cy.visit('/admin/process-instances/find-by-id'); + cy.get('#process-instance-id-input').type(processInstanceId); + + cy.get('button') + .contains(/^Submit$/) + .click(); + + cy.wait(2000); + cy.get('#tag-1 > span').contains('complete'); +} + // Equipment Path - Without Files describe.only('Equipment Path - Without Files', () => { Cypress._.times(1, () => { @@ -270,6 +287,7 @@ describe.only('Equipment Path - Without Files', () => { 'Task: Reminder: Check Existing Budget', 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -397,6 +415,7 @@ describe.only('Equipment Path - Without Files', () => { null, 'reject' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -550,6 +569,7 @@ describe.only('Equipment Path - Without Files', () => { 'Task: Reminder: Check Existing Budget', 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -663,6 +683,7 @@ describe.only('Equipment Path - Without Files', () => { null, 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -776,6 +797,7 @@ describe.only('Equipment Path - Without Files', () => { null, 'reject' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -907,6 +929,7 @@ describe.only('Equipment Path - Without Files', () => { null, 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); // Within Policy. People Ops Partner Group and Budget owner approves the request @@ -1032,6 +1055,7 @@ describe.only('Equipment Path - Without Files', () => { 'Task: Reminder: Check Existing Budget', 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -1158,6 +1182,7 @@ describe.only('Equipment Path - Without Files', () => { null, 'reject' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -1302,6 +1327,7 @@ describe.only('Equipment Path - Without Files', () => { null, 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); }); @@ -1526,6 +1552,7 @@ describe('Equipment Path - With Files', () => { 'Task: Reminder: Check Existing Budget', 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -1702,6 +1729,7 @@ describe('Equipment Path - With Files', () => { null, 'reject' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -1904,6 +1932,7 @@ describe('Equipment Path - With Files', () => { 'Task: Reminder: Check Existing Budget', 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -2067,6 +2096,7 @@ describe('Equipment Path - With Files', () => { null, 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -2230,6 +2260,7 @@ describe('Equipment Path - With Files', () => { null, 'reject' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -2410,6 +2441,7 @@ describe('Equipment Path - With Files', () => { null, 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); // Within Policy. People Ops Partner Group and Budget owner approves the request @@ -2584,6 +2616,7 @@ describe('Equipment Path - With Files', () => { 'Task: Reminder: Check Existing Budget', 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -2759,6 +2792,7 @@ describe('Equipment Path - With Files', () => { null, 'reject' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -2962,6 +2996,7 @@ describe('Equipment Path - With Files', () => { 'Task: Reminder: Check Existing Budget', 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); }); diff --git a/spiffworkflow-frontend/cypress/pilot/NDR_PP1/learninganddev.cy.js b/spiffworkflow-frontend/cypress/pilot/NDR_PP1/learninganddev.cy.js index c068fa32..b4d9b98c 100644 --- a/spiffworkflow-frontend/cypress/pilot/NDR_PP1/learninganddev.cy.js +++ b/spiffworkflow-frontend/cypress/pilot/NDR_PP1/learninganddev.cy.js @@ -76,24 +76,42 @@ const submitWithUser = ( .contains(/^Submit$/) .click(); - if (expectAdditionalApprovalInfoPage) { - cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 }); + // if (expectAdditionalApprovalInfoPage) { + // cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 }); - cy.get('button') - .contains(/^Continue$/) - .click(); - } + // cy.get('button') + // .contains(/^Continue$/) + // .click(); + // } - cy.visit('/'); - - cy.location({ timeout: 60000 }).should((loc) => { - expect(loc.pathname).to.eq('/'); - }); - cy.wait(2000); + cy.get('button').contains('Return to Home', { timeout: 60000 }); cy.logout(); - cy.wait(2000); + }; + //Check if the process instance is completed successfully + const checkProcessInstanceCompleted = ( + username, + password, + processInstanceId +) => { + cy.wait(2000); + cy.log('========Login with : ', username); + cy.log('========processInstanceId: ', processInstanceId); + cy.login(username, password); + + cy.wait(1000); + cy.visit('/admin/process-instances/find-by-id'); + cy.get('#process-instance-id-input').type(processInstanceId); + + cy.get('button') + .contains(/^Submit$/) + .click(); + + cy.wait(2000); + cy.get('#tag-1 > span').contains('complete'); +} + // Learning and Development Path - Without Files describe.only('Learning and Development Path - Without Files', () => { Cypress._.times(1, () => { @@ -205,6 +223,7 @@ describe.only('Learning and Development Path - Without Files', () => { null, 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -318,6 +337,7 @@ describe.only('Learning and Development Path - Without Files', () => { null, 'reject' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -449,6 +469,7 @@ describe.only('Learning and Development Path - Without Files', () => { null, 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -604,6 +625,7 @@ describe.only('Learning and Development Path - Without Files', () => { null, 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -717,6 +739,7 @@ describe.only('Learning and Development Path - Without Files', () => { null, 'reject' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -848,6 +871,20 @@ describe.only('Learning and Development Path - Without Files', () => { 'Task: Reminder: Check Existing Budget', 'approve' ); + + const peopleOpsUsername = Cypress.env('peopleopssme_username'); + const peopleOpsPassword = Cypress.env('peopleopssme_password'); + cy.log(`=====peopleOpsUsername : ${peopleOpsUsername}`); + cy.log(`=====peopleOpsPassword : ${peopleOpsPassword}`); + + submitWithUser( + peopleOpsUsername, + peopleOpsPassword, + processInstanceId, + null, + 'approve' + ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); }); @@ -1017,6 +1054,7 @@ describe('Learning and Development Path - With Files', () => { null, 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -1181,6 +1219,7 @@ describe('Learning and Development Path - With Files', () => { null, 'reject' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -1362,6 +1401,7 @@ describe('Learning and Development Path - With Files', () => { null, 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -1568,6 +1608,7 @@ describe('Learning and Development Path - With Files', () => { null, 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -1732,6 +1773,7 @@ describe('Learning and Development Path - With Files', () => { null, 'reject' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -1927,6 +1969,7 @@ describe('Learning and Development Path - With Files', () => { null, 'approve' ); + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); }); diff --git a/spiffworkflow-frontend/cypress/pilot/NDR_PP1/otherfees.cy.js b/spiffworkflow-frontend/cypress/pilot/NDR_PP1/otherfees.cy.js index c3d7f2d9..323116b3 100644 --- a/spiffworkflow-frontend/cypress/pilot/NDR_PP1/otherfees.cy.js +++ b/spiffworkflow-frontend/cypress/pilot/NDR_PP1/otherfees.cy.js @@ -76,24 +76,41 @@ const submitWithUser = ( .contains(/^Submit$/) .click(); - if (expectAdditionalApprovalInfoPage) { - cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 }); + // if (expectAdditionalApprovalInfoPage) { + // cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 }); - cy.get('button') - .contains(/^Continue$/) - .click(); - } + // cy.get('button') + // .contains(/^Continue$/) + // .click(); + // } - cy.visit('/'); - - cy.location({ timeout: 60000 }).should((loc) => { - expect(loc.pathname).to.eq('/'); - }); - cy.wait(2000); + cy.get('button').contains('Return to Home', { timeout: 60000 }); cy.logout(); - cy.wait(2000); }; + //Check if the process instance is completed successfully + const checkProcessInstanceCompleted = ( + username, + password, + processInstanceId +) => { + cy.wait(2000); + cy.log('========Login with : ', username); + cy.log('========processInstanceId: ', processInstanceId); + cy.login(username, password); + + cy.wait(1000); + cy.visit('/admin/process-instances/find-by-id'); + cy.get('#process-instance-id-input').type(processInstanceId); + + cy.get('button') + .contains(/^Submit$/) + .click(); + + cy.wait(2000); + cy.get('#tag-1 > span').contains('complete'); +} + describe.only('Other Fees Path - Without Files', () => { Cypress._.times(1, () => { // Budget owner approves the request @@ -219,6 +236,8 @@ describe.only('Other Fees Path - Without Files', () => { 'Task: Reminder: Check Existing Budget', 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -332,6 +351,8 @@ describe.only('Other Fees Path - Without Files', () => { null, 'reject' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -463,6 +484,8 @@ describe.only('Other Fees Path - Without Files', () => { 'Task: Reminder: Check Existing Budget', 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); }); @@ -644,6 +667,8 @@ describe('Other Fees Path - With Files', () => { 'Task: Reminder: Check Existing Budget', 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -808,6 +833,8 @@ describe('Other Fees Path - With Files', () => { null, 'reject' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -990,6 +1017,8 @@ describe('Other Fees Path - With Files', () => { 'Task: Reminder: Check Existing Budget', 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); }); diff --git a/spiffworkflow-frontend/cypress/pilot/NDR_PP1/softwarelicense.cy.js b/spiffworkflow-frontend/cypress/pilot/NDR_PP1/softwarelicense.cy.js index b02213c8..d68ff69a 100644 --- a/spiffworkflow-frontend/cypress/pilot/NDR_PP1/softwarelicense.cy.js +++ b/spiffworkflow-frontend/cypress/pilot/NDR_PP1/softwarelicense.cy.js @@ -76,19 +76,42 @@ const submitWithUser = ( .contains(/^Submit$/) .click(); - // if (expectAdditionalApprovalInfoPage) { - // cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 }); - // - // cy.get('button') - // .contains(/^Continue$/) - // .click(); - // } + if (expectAdditionalApprovalInfoPage === 'Task: Update Application Landscape') { + cy.contains(expectAdditionalApprovalInfoPage, { timeout: 60000 }); + + cy.get('button') + .contains(/^Continue$/) + .click(); + } // cy.getBySel('return-to-home-button', { timeout: 60000 }); cy.get('button').contains('Return to Home', { timeout: 60000 }); cy.logout(); }; + //Check if the process instance is completed successfully + const checkProcessInstanceCompleted = ( + username, + password, + processInstanceId + ) => { + cy.wait(2000); + cy.log('========Login with : ', username); + cy.log('========processInstanceId: ', processInstanceId); + cy.login(username, password); + + cy.wait(1000); + cy.visit('/admin/process-instances/find-by-id'); + cy.get('#process-instance-id-input').type(processInstanceId); + + cy.get('button') + .contains(/^Submit$/) + .click(); + + cy.wait(2000); + cy.get('#tag-1 > span').contains('complete'); + } + // Software and Licenses Path - Without Files describe.only('Software and Licenses Path - Without Files', () => { Cypress._.times(1, () => { @@ -226,6 +249,8 @@ describe.only('Software and Licenses Path - Without Files', () => { null, 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -390,11 +415,13 @@ describe.only('Software and Licenses Path - Without Files', () => { null, 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); // Budget owner rejects the request - it('Budget owner rejects', () => { + it.only('Budget owner rejects', () => { const username = Cypress.env('requestor_username'); const password = Cypress.env('requestor_password'); cy.log(`=====username : ${username}`); @@ -498,6 +525,8 @@ describe.only('Software and Licenses Path - Without Files', () => { null, 'reject' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -645,6 +674,8 @@ describe.only('Software and Licenses Path - Without Files', () => { null, 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -786,6 +817,8 @@ describe.only('Software and Licenses Path - Without Files', () => { null, 'reject' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); }); @@ -912,6 +945,8 @@ describe('Software and Licenses Path - Without Files and with only mandatory fi null, 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -1049,6 +1084,8 @@ describe('Software and Licenses Path - Without Files and with only mandatory fi null, 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -1145,6 +1182,8 @@ describe('Software and Licenses Path - Without Files and with only mandatory fi null, 'reject' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -1280,6 +1319,8 @@ describe('Software and Licenses Path - Without Files and with only mandatory fi null, 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -1409,6 +1450,8 @@ describe('Software and Licenses Path - Without Files and with only mandatory fi null, 'reject' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); }); @@ -1614,6 +1657,8 @@ describe('Software and Licenses Path - With Files', () => { null, 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -1817,6 +1862,8 @@ describe('Software and Licenses Path - With Files', () => { null, 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -1979,6 +2026,8 @@ describe('Software and Licenses Path - With Files', () => { null, 'reject' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -2181,6 +2230,8 @@ describe('Software and Licenses Path - With Files', () => { null, 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -2378,6 +2429,8 @@ describe('Software and Licenses Path - With Files', () => { null, 'reject' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); }); @@ -2602,6 +2655,8 @@ describe('Software and Licenses Path - With Files and Multiple items', () => { null, 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -2836,6 +2891,8 @@ describe('Software and Licenses Path - With Files and Multiple items', () => { null, 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -3030,6 +3087,8 @@ describe('Software and Licenses Path - With Files and Multiple items', () => { null, 'reject' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -3261,6 +3320,8 @@ describe('Software and Licenses Path - With Files and Multiple items', () => { null, 'approve' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); @@ -3481,6 +3542,8 @@ describe('Software and Licenses Path - With Files and Multiple items', () => { null, 'reject' ); + + checkProcessInstanceCompleted(username, password, processInstanceId); }); }); }); diff --git a/spiffworkflow-frontend/package-lock.json b/spiffworkflow-frontend/package-lock.json index 9642f5f9..c2900d30 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 85e00d59..5b85e25c 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/public/interstitial/completed.png b/spiffworkflow-frontend/public/interstitial/completed.png deleted file mode 100644 index 5dafcebc..00000000 Binary files a/spiffworkflow-frontend/public/interstitial/completed.png and /dev/null differ diff --git a/spiffworkflow-frontend/public/interstitial/errored.png b/spiffworkflow-frontend/public/interstitial/errored.png deleted file mode 100644 index a9aaba1e..00000000 Binary files a/spiffworkflow-frontend/public/interstitial/errored.png and /dev/null differ diff --git a/spiffworkflow-frontend/public/interstitial/locked.png b/spiffworkflow-frontend/public/interstitial/locked.png deleted file mode 100644 index 5c7ab758..00000000 Binary files a/spiffworkflow-frontend/public/interstitial/locked.png and /dev/null differ diff --git a/spiffworkflow-frontend/public/interstitial/redirect.png b/spiffworkflow-frontend/public/interstitial/redirect.png deleted file mode 100644 index 2d81b3f5..00000000 Binary files a/spiffworkflow-frontend/public/interstitial/redirect.png and /dev/null differ diff --git a/spiffworkflow-frontend/public/interstitial/waiting.png b/spiffworkflow-frontend/public/interstitial/waiting.png deleted file mode 100644 index 0701cdc9..00000000 Binary files a/spiffworkflow-frontend/public/interstitial/waiting.png and /dev/null differ diff --git a/spiffworkflow-frontend/src/App.tsx b/spiffworkflow-frontend/src/App.tsx index 8031802c..cddc3061 100644 --- a/spiffworkflow-frontend/src/App.tsx +++ b/spiffworkflow-frontend/src/App.tsx @@ -3,9 +3,11 @@ import { Content } from '@carbon/react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { defineAbility } from '@casl/ability'; +import React from 'react'; 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'; @@ -14,6 +16,7 @@ import { AbilityContext } from './contexts/Can'; import UserService from './services/UserService'; import ErrorDisplay from './components/ErrorDisplay'; import APIErrorProvider from './contexts/APIErrorContext'; +import ScrollToTop from './components/ScrollToTop'; export default function App() { if (!UserService.isLoggedIn()) { @@ -31,10 +34,12 @@ export default function App() { + } /> + } /> } /> } /> } /> diff --git a/spiffworkflow-frontend/src/components/ErrorDisplay.tsx b/spiffworkflow-frontend/src/components/ErrorDisplay.tsx index 40116973..67e6e18d 100644 --- a/spiffworkflow-frontend/src/components/ErrorDisplay.tsx +++ b/spiffworkflow-frontend/src/components/ErrorDisplay.tsx @@ -109,7 +109,6 @@ export default function ErrorDisplay() { if (errorObject) { const title = 'Error:'; - window.scrollTo(0, 0); // Scroll back to the top of the page errorTag = ( removeError()} type="error"> diff --git a/spiffworkflow-frontend/src/components/NavigationBar.tsx b/spiffworkflow-frontend/src/components/NavigationBar.tsx index b00bee84..b9301d9f 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,15 @@ export default function NavigationBar() { return activeKey === menuItemPath; }; + let aboutLinkElement = null; + + if (Object.keys(versionInfo).length) { + aboutLinkElement = About; + } + + const userEmail = UserService.getUserEmail(); + const username = UserService.getPreferredUsername(); + const profileToggletip = (
@@ -89,15 +108,18 @@ export default function NavigationBar() { className="user-profile-toggletip-button" type="button" > -
- {UserService.getPreferredUsername()[0].toUpperCase()} -
+
{username[0].toUpperCase()}

- {UserService.getPreferredUsername()} + {username}

-

{UserService.getUserEmail()}

+ {username !== userEmail &&

{userEmail}

} +
+ {aboutLinkElement} + + Documentation +
); diff --git a/spiffworkflow-frontend/src/components/ProcessInterstitial.tsx b/spiffworkflow-frontend/src/components/ProcessInterstitial.tsx index 7dcc3326..9b26122d 100644 --- a/spiffworkflow-frontend/src/components/ProcessInterstitial.tsx +++ b/spiffworkflow-frontend/src/components/ProcessInterstitial.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { fetchEventSource } from '@microsoft/fetch-event-source'; // @ts-ignore -import { Loading, Grid, Column, Button } from '@carbon/react'; +import { Loading, Button } from '@carbon/react'; import { BACKEND_BASE_URL } from '../config'; import { getBasicHeaders } from '../services/HttpService'; @@ -95,26 +95,17 @@ export default function ProcessInterstitial({ return state; }; - const getStatusImage = () => { - switch (getStatus()) { - case 'RUNNING': - return ( - - ); - case 'LOCKED': - return Locked; - case 'READY': - case 'REDIRECTING': - return Redirecting ....; - case 'WAITING': - return Waiting ....; - case 'COMPLETED': - return Completed; - case 'ERROR': - return Errored; - default: - return getStatus(); + const getLoadingIcon = () => { + if (getStatus() === 'RUNNING') { + return ( + + ); } + return null; }; const getReturnHomeButton = (index: number) => { @@ -129,6 +120,7 @@ export default function ProcessInterstitial({ kind="secondary" data-qa="return-to-home-button" onClick={() => navigate(`/tasks`)} + style={{ marginBottom: 30 }} > Return to Home @@ -138,35 +130,14 @@ export default function ProcessInterstitial({ return ''; }; - const getHr = (index: number) => { - if (index === 0) { - return ( -
-
-
- ); - } - return ''; - }; - - function capitalize(str: string): string { - if (str && str.length > 0) { - return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); - } - return ''; - } - const userMessage = (myTask: ProcessInstanceTask) => { if (!processInstance || processInstance.status === 'completed') { if (!myTask.can_complete && userTasks.includes(myTask.type)) { return ( - <> -

Waiting on Someone Else

-

- This next task is assigned to a different person or team. There is - no action for you to take at this time. -

- +

+ This next task is assigned to a different person or team. There is + no action for you to take at this time. +

); } if (shouldRedirect(myTask)) { @@ -198,6 +169,7 @@ export default function ProcessInterstitial({ if (state === 'CLOSED' && lastTask === null) { navigate(`/tasks`); } + if (lastTask) { return ( <> @@ -215,21 +187,10 @@ export default function ProcessInterstitial({ ], ]} /> -
- {getStatusImage()} -
-

- {lastTask.process_model_display_name}:{' '} - {lastTask.process_instance_id} -

-
Status: {capitalize(getStatus())}
-
-
-
-
- {data.map((d, index) => ( - - + {getLoadingIcon()} +
+ {data.map((d, index) => ( + <>
{getReturnHomeButton(index)} - {getHr(index)} - - - ))} + + ))} +
); } diff --git a/spiffworkflow-frontend/src/components/ScrollToTop.tsx b/spiffworkflow-frontend/src/components/ScrollToTop.tsx new file mode 100644 index 00000000..7d8c6f1a --- /dev/null +++ b/spiffworkflow-frontend/src/components/ScrollToTop.tsx @@ -0,0 +1,12 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +export default function ScrollToTop() { + const { pathname } = useLocation(); + + useEffect(() => { + window.scrollTo(0, 0); + }, [pathname]); + + return null; +} diff --git a/spiffworkflow-frontend/src/helpers/appVersionInfo.ts b/spiffworkflow-frontend/src/helpers/appVersionInfo.ts new file mode 100644 index 00000000..28de1d65 --- /dev/null +++ b/spiffworkflow-frontend/src/helpers/appVersionInfo.ts @@ -0,0 +1,22 @@ +import { ObjectWithStringKeysAndValues } from '../interfaces'; + +const appVersionInfo = () => { + const versionInfoFromHtmlMetaTag = document.querySelector( + 'meta[name="version-info"]' + ); + let versionInfo: ObjectWithStringKeysAndValues = {}; + if (versionInfoFromHtmlMetaTag) { + const versionInfoContentString = + versionInfoFromHtmlMetaTag.getAttribute('content'); + if ( + versionInfoContentString && + versionInfoContentString !== '%REACT_APP_VERSION_INFO%' + ) { + versionInfo = JSON.parse(versionInfoContentString); + } + } + + return versionInfo; +}; + +export default appVersionInfo; diff --git a/spiffworkflow-frontend/src/hooks/useEffectDebugger.tsx b/spiffworkflow-frontend/src/hooks/useEffectDebugger.tsx new file mode 100644 index 00000000..facdce38 --- /dev/null +++ b/spiffworkflow-frontend/src/hooks/useEffectDebugger.tsx @@ -0,0 +1,44 @@ +import { useEffect, useRef } from 'react'; + +// https://stackoverflow.com/a/59843241/6090676 +// useful for determining what state has changed that caused useEffect to trigger. +// just change the useEffect in question to useEffectDebugger +const usePrevious = (value: any, initialValue: any) => { + const ref = useRef(initialValue); + useEffect(() => { + ref.current = value; + }); + return ref.current; +}; +export default function useEffectDebugger( + effectHook: any, + dependencies: any, + dependencyNames: any = [] +) { + const previousDeps = usePrevious(dependencies, []); + + const changedDeps = dependencies.reduce( + (accum: any, dependency: any, index: any) => { + if (dependency !== previousDeps[index]) { + const keyName = dependencyNames[index] || index; + return { + ...accum, + [keyName]: { + before: previousDeps[index], + after: dependency, + }, + }; + } + + return accum; + }, + {} + ); + + if (Object.keys(changedDeps).length) { + console.log('[use-effect-debugger] ', changedDeps); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(effectHook, dependencies); +} diff --git a/spiffworkflow-frontend/src/index.css b/spiffworkflow-frontend/src/index.css index b2167a89..cfea25f4 100644 --- a/spiffworkflow-frontend/src/index.css +++ b/spiffworkflow-frontend/src/index.css @@ -478,22 +478,28 @@ svg.notification-icon { .user_instructions_0 { filter: opacity(1); + font-size: 1.2em; + margin-bottom: 30px; } .user_instructions_1 { filter: opacity(60%); + font-size: 1.1em; } .user_instructions_2 { filter: opacity(40%); + font-size: 1em; } .user_instructions_3 { filter: opacity(20%); + font-size: 9em; } .user_instructions_4 { filter: opacity(10%); + font-size: 8em; } .float-right { @@ -562,3 +568,7 @@ svg.notification-icon { .user-circle:nth-child(n+11) { background-color: #8e8e8e; } + +.version-info-column { + width: 50%; +} diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts index e9c5f0df..6f480fd8 100644 --- a/spiffworkflow-frontend/src/interfaces.ts +++ b/spiffworkflow-frontend/src/interfaces.ts @@ -112,6 +112,8 @@ export interface ProcessReference { is_primary: boolean; } +export type ObjectWithStringKeysAndValues = { [key: string]: string }; + export interface ProcessFile { content_type: string; last_modified: string; diff --git a/spiffworkflow-frontend/src/routes/About.tsx b/spiffworkflow-frontend/src/routes/About.tsx new file mode 100644 index 00000000..786521d8 --- /dev/null +++ b/spiffworkflow-frontend/src/routes/About.tsx @@ -0,0 +1,65 @@ +// @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(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) + .sort() + .map((key) => { + const value = versionInfoDict[key]; + return ( + + + {key} + + {value} + + ); + }); + return ( + <> +

+ {title} +

+ + {tableRows} +
+ + ); + } + return null; + }; + + return ( +
+

About

+ {versionInfoFromDict('Frontend version information', frontendVersionInfo)} + {versionInfoFromDict('Backend version information', backendVersionInfo)} +
+ ); +} diff --git a/spiffworkflow-frontend/src/routes/InProgressInstances.tsx b/spiffworkflow-frontend/src/routes/InProgressInstances.tsx index 2e17cfc5..0ad926ec 100644 --- a/spiffworkflow-frontend/src/routes/InProgressInstances.tsx +++ b/spiffworkflow-frontend/src/routes/InProgressInstances.tsx @@ -22,7 +22,7 @@ export default function InProgressInstances() { const titleText = `This is a list of instances with tasks that are waiting for the ${userGroup} group.`; const headerElement = (

- Instances with tasks waiting for {userGroup} + Waiting for {userGroup}

); return ( @@ -61,7 +61,7 @@ export default function InProgressInstances() { 'This is a list of instances that have tasks that you can complete.'; const waitingForMeHeaderElement = (

- Instances with tasks waiting for me + Waiting for me

); diff --git a/spiffworkflow-frontend/src/routes/TaskShow.tsx b/spiffworkflow-frontend/src/routes/TaskShow.tsx index 1eb30fac..6f1407d5 100644 --- a/spiffworkflow-frontend/src/routes/TaskShow.tsx +++ b/spiffworkflow-frontend/src/routes/TaskShow.tsx @@ -88,19 +88,15 @@ function TypeaheadWidget({ ); } -enum FormSubmitType { - Default, - Draft, -} - export default function TaskShow() { const [task, setTask] = useState(null); const [userTasks] = useState(null); const params = useParams(); const navigate = useNavigate(); const [disabled, setDisabled] = useState(false); - // save current form data so that we can avoid validations in certain situations - const [currentFormObject, setCurrentFormObject] = useState({}); + const [noValidate, setNoValidate] = useState(false); + + const [taskData, setTaskData] = useState(null); const { addError, removeError } = useAPIError(); @@ -115,11 +111,11 @@ export default function TaskShow() { useEffect(() => { const processResult = (result: Task) => { setTask(result); + setTaskData(result.data); setDisabled(false); if (!result.can_complete) { navigateToInterstitial(result); } - window.scrollTo(0, 0); // Scroll back to the top of the page /* Disable call to load previous tasks -- do not display menu. const url = `/v1.0/process-instances/for-me/${modifyProcessIdentifierForPathParam( @@ -167,26 +163,30 @@ export default function TaskShow() { } }; - const handleFormSubmit = ( - formObject: any, - _event: any, - submitType: FormSubmitType = FormSubmitType.Default - ) => { + const handleFormSubmit = (formObject: any, _event: any) => { if (disabled) { return; } + const dataToSubmit = formObject?.formData; if (!dataToSubmit) { navigate(`/tasks`); return; } let queryParams = ''; - if (submitType === FormSubmitType.Draft) { + + // if validations are turned off then save as draft + if (noValidate) { queryParams = '?save_as_draft=true'; } setDisabled(true); removeError(); delete dataToSubmit.isManualTask; + + // NOTE: rjsf sets blanks values to undefined and JSON.stringify removes keys with undefined values + // so there is no way to clear out a field that previously had a value. + // To resolve this, we could potentially go through the object that we are posting (either in here or in + // HttpService) and translate all undefined values to null. HttpService.makeCallToBackend({ path: `/tasks/${params.process_instance_id}/${params.task_id}${queryParams}`, successCallback: processSubmitResult, @@ -301,9 +301,16 @@ export default function TaskShow() { return errors; }; - const updateFormData = (formObject: any) => { - currentFormObject.formData = formObject.formData; - setCurrentFormObject(currentFormObject); + // This turns off validations and then dispatches the click event after + // waiting a second to give the state time to update. + // This is to allow saving the form without validations causing issues. + const handleSaveAndCloseButton = () => { + setNoValidate(true); + setTimeout(() => { + (document.getElementById('our-very-own-form') as any).dispatchEvent( + new Event('submit', { cancelable: true, bubbles: true }) + ); + }, 1000); }; const formElement = () => { @@ -312,11 +319,9 @@ export default function TaskShow() { } let formUiSchema; - let taskData = task.data; let jsonSchema = task.form_schema; let reactFragmentToHideSubmitButton = null; if (task.typename === 'ManualTask') { - taskData = {}; jsonSchema = { type: 'object', required: [], @@ -357,14 +362,12 @@ export default function TaskShow() { closeButton = ( ); } @@ -399,17 +402,18 @@ export default function TaskShow() {
setTaskData(obj.formData)} onSubmit={handleFormSubmit} schema={jsonSchema} uiSchema={formUiSchema} widgets={widgets} validator={validator} - onChange={updateFormData} customValidate={customValidate} + noValidate={noValidate} omitExtraData - liveOmit > {reactFragmentToHideSubmitButton}
diff --git a/spiffworkflow-frontend/src/services/HttpService.ts b/spiffworkflow-frontend/src/services/HttpService.ts index 83d71af8..4dffa154 100644 --- a/spiffworkflow-frontend/src/services/HttpService.ts +++ b/spiffworkflow-frontend/src/services/HttpService.ts @@ -56,6 +56,7 @@ backendCallProps) => { Object.assign(httpArgs, { body: postBody }); } else if (typeof postBody === 'object') { if (!objectIsEmpty(postBody)) { + // NOTE: stringify strips out keys with value undefined Object.assign(httpArgs, { body: JSON.stringify(postBody) }); Object.assign(headers, { 'Content-Type': 'application/json' }); } diff --git a/spiffworkflow-frontend/src/themes/carbon/BaseInputTemplate/BaseInputTemplate.tsx b/spiffworkflow-frontend/src/themes/carbon/BaseInputTemplate/BaseInputTemplate.tsx index cf153157..6e8af43a 100644 --- a/spiffworkflow-frontend/src/themes/carbon/BaseInputTemplate/BaseInputTemplate.tsx +++ b/spiffworkflow-frontend/src/themes/carbon/BaseInputTemplate/BaseInputTemplate.tsx @@ -100,12 +100,15 @@ export default function BaseInputTemplate< if (type === 'date') { // display the date in a date input box as the config wants. // it should in be y-m-d when it gets here. - let dateValue: string | null = ''; + let dateValue: string | null = value; if (value || value === 0) { if (value.length < 10) { dateValue = value; } else { - dateValue = ymdDateStringToConfiguredFormat(value); + try { + dateValue = ymdDateStringToConfiguredFormat(value); + // let the date component and form validators handle bad dates and do not blow up + } catch (RangeError) {} } }