Use vite to build (#1390)

* some base updates for vite w/ burnettk

* i can log in w/ burnettk

* a couple more fixes w/ burnettk

* make sure selectedTabIndex has been set before using it w/ burnettk

* fixed active-users db issue, added type module to package json to fix prerender issues, and various other issues w/ burnettk

* fixed issues with building and running from compiled w/ burnettk

* pyl

* eslint fix is running and removed both inferno and navigationBar warnings

* vim likes the Dockerfile suffix by default

* use process.env.HOME

* probably do not need alias

* a little clean up and fixed font warnings w/ burnettk

* updated elements to remove warnings in the console w/ burnettk

* fixed es lint issues w/ burnettk

* update docker build in frontend w/ burnettk

* set the specific tag of nginx w/ burnettk

* build docker imgaes for this branch to test w/ burnettk

* added vitest and updated Dockerfile to be nginx w/ burnettk

* tests are passing w/ burnettk

* add prefresh and more keys

* added cypress-vite to attempt to get cypress working again

* some coderabbit suggestions

* hopefully there is no reason to use PUBLIC_URL at all when using vite

* use the correct location of the index file in the docker image

* spaces are fine in index.html file variable declaration

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
Co-authored-by: burnettk <burnettk@users.noreply.github.com>
This commit is contained in:
jasquat 2024-04-15 18:22:34 +00:00 committed by GitHub
parent fd11e627f2
commit 9147a8db8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 5366 additions and 803 deletions

View File

@ -32,6 +32,7 @@ on:
- main
- spiffdemo
- GSA-TTS-fix-path-routing-in-generated-openid-urls
- use-vite-to-build
jobs:
create_frontend_docker_image:

View File

@ -2,6 +2,7 @@ import json
import time
import flask.wrappers
import sqlalchemy
from flask import g
from flask import jsonify
from flask import make_response
@ -13,17 +14,30 @@ from spiffworkflow_backend.models.user import UserModel
def active_user_updates(last_visited_identifier: str) -> Response:
active_user = ActiveUserModel.query.filter_by(user_id=g.user.id, last_visited_identifier=last_visited_identifier).first()
current_time = round(time.time())
query_args = {"user_id": g.user.id, "last_visited_identifier": last_visited_identifier}
active_user = ActiveUserModel.query.filter_by(**query_args).first()
if active_user is None:
active_user = ActiveUserModel(
user_id=g.user.id, last_visited_identifier=last_visited_identifier, last_seen_in_seconds=round(time.time())
user_id=g.user.id, last_visited_identifier=last_visited_identifier, last_seen_in_seconds=current_time
)
db.session.add(active_user)
try:
db.session.commit()
active_user.last_seen_in_seconds = round(time.time())
db.session.add(active_user)
except sqlalchemy.exc.IntegrityError:
# duplicate entry. two processes are trying to create the same entry at the same time. it is fine to drop one request.
db.session.rollback()
else:
try:
db.session.query(ActiveUserModel).filter_by(**query_args).update({"last_seen_in_seconds": current_time})
db.session.commit()
except sqlalchemy.exc.OperationalError as exception:
if "Deadlock" in str(exception):
# two processes are trying to update the same entry at the same time. it is fine to drop one request.
db.session.rollback()
else:
raise
cutoff_time_in_seconds = time.time() - 30
active_users = (

View File

@ -13,6 +13,7 @@
# production
/build
/dist
# misc
.DS_Store

View File

@ -44,19 +44,17 @@ RUN ./bin/build
######################## - FINAL
# Final image without setup dependencies.
FROM base AS final
# Use nginx as the base image
FROM nginx:1.25.4-bookworm
LABEL description="Frontend component of SpiffWorkflow, a software development platform for building, running, and monitoring executable diagrams"
# Remove default nginx configuration
RUN rm -rf /etc/nginx/conf.d/*
# WARNING: On localhost frontend assumes backend is one port lower.
ENV PORT0=7001
# Copy the nginx configuration file
COPY docker_build/nginx.conf.template /var/tmp
COPY --from=setup /app/build /app/build
# Copy the built static files into the nginx directory
COPY --from=setup /app/dist /usr/share/nginx/html
COPY --from=setup /app/bin /app/bin
COPY --from=setup /app/node_modules.justserve /app/node_modules
RUN chown -R node:node /app
USER node
CMD ["/app/bin/boot_server_in_docker"]

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash
function error_handler() {
>&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
echo >&2 "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}."
exit "$2"
}
trap 'error_handler ${LINENO} $?' ERR
@ -10,19 +10,19 @@ set -o errtrace -o errexit -o nounset -o pipefail
# sort of like https://lithic.tech/blog/2020-05/react-dynamic-config, but without golang
react_configs=$(env | grep -E "^SPIFFWORKFLOW_FRONTEND_RUNTIME_CONFIG_" || echo '')
if [[ -n "$react_configs" ]]; then
index_html_file="./build/index.html"
index_html_file="/usr/share/nginx/html/index.html"
if [[ ! -f "$index_html_file" ]]; then
>&2 echo "ERROR: Could not find '${index_html_file}'. Cannot use SPIFFWORKFLOW_FRONTEND_RUNTIME_CONFIG values without it."
echo >&2 "ERROR: Could not find '${index_html_file}'. Cannot use SPIFFWORKFLOW_FRONTEND_RUNTIME_CONFIG values without it."
exit 1
fi
if ! command -v sed >/dev/null ; then
>&2 echo "ERROR: sed command not found. Cannot use SPIFFWORKFLOW_FRONTEND_RUNTIME_CONFIG values without it."
if ! command -v sed >/dev/null; then
echo >&2 "ERROR: sed command not found. Cannot use SPIFFWORKFLOW_FRONTEND_RUNTIME_CONFIG values without it."
exit 1
fi
if ! command -v perl >/dev/null ; then
>&2 echo "ERROR: perl command not found. Cannot use SPIFFWORKFLOW_FRONTEND_RUNTIME_CONFIG values without it."
if ! command -v perl >/dev/null; then
echo >&2 "ERROR: perl command not found. Cannot use SPIFFWORKFLOW_FRONTEND_RUNTIME_CONFIG values without it."
exit 1
fi
@ -30,19 +30,25 @@ if [[ -n "$react_configs" ]]; then
react_config_without_prefix=$(sed -E 's/^SPIFFWORKFLOW_FRONTEND_RUNTIME_CONFIG_([^=]*)=(.*)/\1=\\"\2\\"/' <<<"${react_config}")
if [[ -z "$react_config_without_prefix" ]]; then
>&2 echo "ERROR: Could not parse react config line: '${react_config}'."
echo >&2 "ERROR: Could not parse react config line: '${react_config}'."
exit 1
fi
escaped_react_config=$(sed -E 's|/|\\/|g' <<<"${react_config_without_prefix}")
# actually do the search and replace to add the js config to the html page
perl -pi -e "s/(window.spiffworkflowFrontendJsenv=\{\})/\1;window.spiffworkflowFrontendJsenv.${escaped_react_config}/" "$index_html_file"
perl -pi -e "s/(window.spiffworkflowFrontendJsenv *= *\{\})/\1;window.spiffworkflowFrontendJsenv.${escaped_react_config}/" "$index_html_file"
if ! grep -Eq "${react_config_without_prefix}" "$index_html_file"; then
>&2 echo "ERROR: Could not find '${react_config_without_prefix}' in '${index_html_file}' after search and replace. It is likely that the assumptions in boot_server_in_docker about the contents of the html page have changed. Fix the glitch in boot_server_in_docker."
echo >&2 "ERROR: Could not find '${react_config_without_prefix}' in '${index_html_file}' after search and replace. It is likely that the assumptions in boot_server_in_docker about the contents of the html page have changed. Fix the glitch in boot_server_in_docker."
exit 1
fi
done
fi
exec ./node_modules/.bin/serve -s build -l "$PORT0"
port_to_use="${PORT0:-80}"
if [[ -n "${SPIFFWORKFLOW_FRONTEND_INTERNAL_PORT:-}" ]]; then
port_to_use="$SPIFFWORKFLOW_FRONTEND_INTERNAL_PORT"
fi
perl -p -e "s/{{SPIFFWORKFLOW_FRONTEND_INTERNAL_PORT}}/${port_to_use}/" /var/tmp/nginx.conf.template >/etc/nginx/conf.d/default.conf
exec nginx -g "daemon off;"

View File

@ -9,7 +9,7 @@ set -o errtrace -o errexit -o nounset -o pipefail
if [[ -f "version_info.json" ]]; then
version_info=$(cat version_info.json)
export REACT_APP_VERSION_INFO="$version_info"
export VITE_VERSION_INFO="$version_info"
fi
npm run build

BIN
spiffworkflow-frontend/bun.lockb Executable file

Binary file not shown.

View File

@ -1,63 +0,0 @@
module.exports = {
module: {
rules: [
{
test: /\.m?[jt]sx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: [
[
'@babel/plugin-transform-react-jsx',
{
pragma: 'h',
pragmaFrag: 'Fragment',
},
],
'@babel/preset-react',
'@babel/plugin-transform-typescript',
{
importSource: '@bpmn-io/properties-panel/preact',
runtime: 'automatic',
},
'@babel/plugin-proposal-class-properties',
{ loose: true },
'@babel/plugin-proposal-private-methods',
{ loose: true },
'@babel/plugin-proposal-private-property-in-object',
{ loose: true },
],
},
},
},
],
},
webpack: {
configure: {
resolve: {
alias: {
inferno:
process.env.NODE_ENV !== 'production'
? 'inferno/dist/index.dev.esm.js'
: 'inferno/dist/index.esm.js',
react: 'preact/compat',
'react-dom/test-utils': 'preact/test-utils',
'react-dom': 'preact/compat', // Must be below test-utils
'react/jsx-runtime': 'preact/jsx-runtime',
},
},
},
},
babel: {
presets: [
'@babel/preset-env',
['@babel/preset-react', { runtime: 'automatic' }],
'@babel/preset-typescript',
],
// plugins: [],
loaderOptions: (babelLoaderOptions) => {
return babelLoaderOptions;
},
},
};

View File

@ -1,6 +1,7 @@
/* eslint-disable */
const { defineConfig } = require('cypress');
const { rm } = require('fs/promises');
import { defineConfig } from 'cypress';
import { rm } from 'fs/promises';
import config from '@cypress/grep/src/plugin';
// yes use video compression in CI, where we will set the env var so we upload to cypress dashboard
const useVideoCompression = !!process.env.CYPRESS_RECORD_KEY;
@ -40,7 +41,6 @@ const cypressConfig = {
baseUrl: spiffWorkflowFrontendUrl,
setupNodeEvents(on, config) {
deleteVideosOnSuccess(on);
require('@cypress/grep/src/plugin')(config);
return config;
},
},
@ -56,4 +56,4 @@ if (!process.env.CYPRESS_RECORD_KEY) {
cypressConfig.videoCompression = false;
}
module.exports = defineConfig(cypressConfig);
export default defineConfig(cypressConfig);

View File

@ -0,0 +1,9 @@
server {
listen {{SPIFFWORKFLOW_FRONTEND_INTERNAL_PORT}};
server_name localhost;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
}

View File

@ -2,38 +2,29 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="version-info" content='%REACT_APP_VERSION_INFO%' />
<meta name="version-info" content='%VITE_VERSION_INFO%' />
<meta
name="description"
content="A turnkey solution for building and executing the workflows that drive your business"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="apple-touch-icon" href="/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="manifest" href="/manifest.json" />
<title>SpiffWorkflow</title>
<script>
window.spiffworkflowFrontendJsenv = {};
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@
"name": "spiffworkflow-frontend",
"version": "0.1.0",
"private": true,
"type": "module",
"dependencies": {
"@babel/core": "^7.24.3",
"@babel/plugin-transform-react-jsx": "^7.18.6",
@ -15,6 +16,7 @@
"@microsoft/fetch-event-source": "^2.0.1",
"@monaco-editor/react": "^4.4.5",
"@mui/material": "^5.10.14",
"@prefresh/vite": "^2.4.5",
"@react-icons/all-files": "^4.1.0",
"@rjsf/core": "5.0.0-beta.20",
"@rjsf/mui": "5.0.0-beta.20",
@ -26,15 +28,15 @@
"@testing-library/react": "^14.2.1",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.12",
"@types/node": "^20.12.6",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@uiw/react-md-editor": "^3.20.2",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.13",
"axios": "^0.27.2",
"bpmn-js": "^13.2.2",
"bpmn-js-properties-panel": "^1.22.0",
"bpmn-js-spiffworkflow": "github:sartography/bpmn-js-spiffworkflow#main",
"bpmn-js-spiffworkflow": "github:sartography/bpmn-js-spiffworkflow#vite-support",
"cookie": "^0.6.0",
"craco": "^0.0.3",
"date-fns": "^3.6.0",
@ -55,11 +57,12 @@
"react-jsonschema-form": "^1.8.1",
"react-router": "^6.22.2",
"react-router-dom": "^6.22.3",
"react-scripts": "^5.0.1",
"serve": "^14.0.0",
"timepicker": "^1.13.18",
"typescript": "^4.7.4",
"use-debounce": "^10.0.0",
"vite": "^5.2.8",
"vite-tsconfig-paths": "^4.3.2",
"web-vitals": "^3.5.2"
},
"overrides": {
@ -68,12 +71,10 @@
}
},
"scripts": {
"start": "ESLINT_NO_DEV_ERRORS=true PORT=7001 craco start",
"docker:start": "ESLINT_NO_DEV_ERRORS=true craco start",
"build": "craco build",
"test": "react-scripts test --coverage",
"t": "npm test -- --watchAll=false",
"eject": "craco eject",
"start": "VITE_VERSION_INFO='{\"version\":\"local\"}' vite",
"build": "vite build",
"serve": "vite preview",
"test": "vitest run --coverage",
"format": "prettier --write src/**/*.[tj]s{,x}",
"lint": "./node_modules/.bin/eslint src",
"lint:fix": "./node_modules/.bin/eslint --fix src"
@ -98,15 +99,19 @@
},
"devDependencies": {
"@cypress/grep": "^3.1.0",
"@preact/preset-vite": "^2.8.2",
"@tanstack/eslint-plugin-query": "^5.28.6",
"@types/carbon__colors": "^10.31.3",
"@types/cookie": "^0.6.0",
"@types/lodash.merge": "^4.6.7",
"@types/node": "^20.12.7",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.62.0",
"@vitest/coverage-v8": "^1.5.0",
"cypress": "^13",
"cypress-file-upload": "^5.0.8",
"cypress-slow-down": "^1.3.1",
"cypress-vite": "^1.5.0",
"eslint": "^8.19.0",
"eslint_d": "^12.2.0",
"eslint-config-airbnb": "^19.0.4",
@ -119,8 +124,11 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-sonarjs": "^0.15.0",
"eslint-plugin-unused-imports": "^2.0.0",
"inherits-browser": "^0.0.1",
"prettier": "^2.7.1",
"safe-regex": "^2.1.1",
"ts-migrate": "^0.1.30"
"tiny-svg": "^2.2.3",
"ts-migrate": "^0.1.30",
"vitest": "^1.5.0"
}
}

View File

@ -66,6 +66,7 @@ export default function ContainerForExtensions() {
extensionUiSchemaFile.file_contents
);
if (
extensionUiSchema &&
extensionUiSchema.ux_elements &&
!extensionUiSchema.disabled
) {

View File

@ -10,7 +10,7 @@ type OwnProps = {
title?: string;
confirmButtonLabel?: string;
kind?: string;
renderIcon?: boolean;
renderIcon?: Element;
iconDescription?: string | null;
hasIconOnly?: boolean;
classNameForModal?: string;
@ -24,7 +24,7 @@ export default function ButtonWithConfirmation({
title = 'Are you sure?',
confirmButtonLabel = 'OK',
kind = 'danger',
renderIcon = false,
renderIcon,
iconDescription = null,
hasIconOnly = false,
classNameForModal,

View File

@ -4,14 +4,12 @@ type OwnProps = {
displayLocation: string;
elementCallback: Function;
extensionUxElements?: UiSchemaUxElement[] | null;
elementCallbackIfNotFound?: Function;
};
export function ExtensionUxElementMap({
displayLocation,
elementCallback,
extensionUxElements,
elementCallbackIfNotFound,
}: OwnProps) {
if (!extensionUxElements) {
return null;
@ -23,15 +21,11 @@ export function ExtensionUxElementMap({
return uxElement.display_location === displayLocation;
}
);
const elementMap = elementsForDisplayLocation.map(
return elementsForDisplayLocation.map(
(uxElement: UiSchemaUxElement, index: number) => {
return elementCallback(uxElement, index);
}
);
if (elementMap.length === 0 && elementCallbackIfNotFound) {
return elementCallbackIfNotFound();
}
return elementMap;
};
return mainElement();
}

View File

@ -41,7 +41,7 @@ export default function Filters({
elements.push(
<Button
onClick={copyReportLink}
kind=""
kind="secondary"
renderIcon={LinkIcon}
iconDescription="Copy shareable link"
hasIconOnly

View File

@ -101,7 +101,11 @@ export default function NavigationBar({ extensionUxElements }: OwnProps) {
const extensionUserProfileElement = (uxElement: UiSchemaUxElement) => {
const navItemPage = `/extensions${uxElement.page}`;
return <Link to={navItemPage}>{uxElement.label}</Link>;
return (
<Link key={navItemPage} to={navItemPage}>
{uxElement.label}
</Link>
);
};
const profileToggletip = () => {
@ -163,6 +167,7 @@ export default function NavigationBar({ extensionUxElements }: OwnProps) {
<HeaderGlobalAction
title={`The current SpiffWorkflow environment is: ${SPIFF_ENVIRONMENT}`}
className="spiff-environment-header-text unclickable-text"
aria-label="our-aria-label"
>
{SPIFF_ENVIRONMENT}
</HeaderGlobalAction>
@ -195,10 +200,10 @@ export default function NavigationBar({ extensionUxElements }: OwnProps) {
return (
<SpiffTooltip title="Manage Secrets and Authentication information for Service Tasks">
<HeaderMenuItem
element={Link}
as={Link}
to="/configuration"
onClick={closeSideNavMenuIfExpanded}
isCurrentPage={isActivePage('/configuration')}
isActive={isActivePage('/configuration')}
>
Configuration
</HeaderMenuItem>
@ -221,11 +226,11 @@ export default function NavigationBar({ extensionUxElements }: OwnProps) {
setActiveKey(navItemPage);
}
return (
<SpiffTooltip title={uxElement?.tooltip}>
<SpiffTooltip key={navItemPage} title={uxElement?.tooltip}>
<HeaderMenuItem
element={Link}
as={Link}
to={navItemPage}
isCurrentPage={isActivePage(navItemPage)}
isActive={isActivePage(navItemPage)}
data-qa={`extension-${slugifyString(
uxElement.label || uxElement.page
)}`}
@ -244,10 +249,10 @@ export default function NavigationBar({ extensionUxElements }: OwnProps) {
<>
<SpiffTooltip title="View and start Process Instances">
<HeaderMenuItem<LinkProps>
element={Link}
as={Link}
to="/"
onClick={closeSideNavMenuIfExpanded}
isCurrentPage={isActivePage('/')}
isActive={isActivePage('/')}
>
<div>Home</div>
</HeaderMenuItem>
@ -256,10 +261,10 @@ export default function NavigationBar({ extensionUxElements }: OwnProps) {
<Can I="GET" a={targetUris.processGroupListPath} ability={ability}>
<SpiffTooltip title="Find and organize Process Groups and Process Models">
<HeaderMenuItem
element={Link}
as={Link}
to={processGroupPath}
onClick={closeSideNavMenuIfExpanded}
isCurrentPage={isActivePage(processGroupPath)}
isActive={isActivePage(processGroupPath)}
data-qa="header-nav-processes"
>
Processes
@ -273,10 +278,10 @@ export default function NavigationBar({ extensionUxElements }: OwnProps) {
>
<SpiffTooltip title="List of active and completed Process Instances">
<HeaderMenuItem
element={Link}
as={Link}
to="/process-instances"
onClick={closeSideNavMenuIfExpanded}
isCurrentPage={isActivePage('/process-instances')}
isActive={isActivePage('/process-instances')}
>
Process Instances
</HeaderMenuItem>
@ -285,10 +290,10 @@ export default function NavigationBar({ extensionUxElements }: OwnProps) {
<Can I="GET" a={targetUris.messageInstanceListPath} ability={ability}>
<SpiffTooltip title="Browse messages being sent and received">
<HeaderMenuItem
element={Link}
as={Link}
to="/messages"
onClick={closeSideNavMenuIfExpanded}
isCurrentPage={isActivePage('/messages')}
isActive={isActivePage('/messages')}
>
Messages
</HeaderMenuItem>
@ -297,10 +302,10 @@ export default function NavigationBar({ extensionUxElements }: OwnProps) {
<Can I="GET" a={targetUris.dataStoreListPath} ability={ability}>
<SpiffTooltip title="Browse data that has been saved to Data Stores">
<HeaderMenuItem
element={Link}
as={Link}
to="/data-stores"
onClick={closeSideNavMenuIfExpanded}
isCurrentPage={isActivePage('/data-stores')}
isActive={isActivePage('/data-stores')}
>
Data Stores
</HeaderMenuItem>
@ -322,12 +327,7 @@ export default function NavigationBar({ extensionUxElements }: OwnProps) {
<HeaderContainer
render={() => (
<Header aria-label="IBM Platform Name" className="cds--g100">
<HeaderName
element={Link}
to="/"
prefix=""
data-qa="spiffworkflow-logo"
>
<HeaderName as={Link} to="/" prefix="" data-qa="spiffworkflow-logo">
<img src={logo} className="app-logo" alt="logo" />
</HeaderName>
</Header>
@ -360,7 +360,7 @@ export default function NavigationBar({ extensionUxElements }: OwnProps) {
isActive={isSideNavExpanded}
/>
<HeaderName
element={Link}
as={Link}
to="/"
onClick={closeSideNavMenuIfExpanded}
prefix=""

View File

@ -77,7 +77,7 @@ export function Notification({
className="cds--inline-notification__close-button"
hasIconOnly
size="sm"
kind=""
kind="ghost"
onClick={onClose}
/>
)}
@ -86,7 +86,7 @@ export function Notification({
data-qa="close-publish-notification"
className="cds--inline-notification__close-button"
size="sm"
kind=""
kind="ghost"
onClick={() => setShowMessage(!showMessage)}
>
{showMessage ? 'Hide' : 'Details'}&nbsp;

View File

@ -65,6 +65,7 @@ export default function ProcessGroupListTiles({
return (
<ClickableTile
id={`process-group-tile-${row.id}`}
key={`process-group-tile-${row.id}`}
className="tile-process-group"
onClick={() =>
navigateToProcessGroup(

View File

@ -99,12 +99,13 @@ export default function ProcessInstanceListSaveAsReport({
onRequestSubmit={addProcessInstanceReport}
onRequestClose={handleSaveFormClose}
hasScrollingContent
aria-label="save perspective"
>
<p className="data-table-description">{descriptionText}</p>
{textInputComponent}
</Modal>
<Button
kind=""
kind="tertiary"
className={buttonClassName}
onClick={() => {
setIdentifier(processInstanceReportSelection?.identifier || '');

View File

@ -386,7 +386,7 @@ export default function ProcessInstanceListTable({
return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<td
key={processInstance.id}
key={`td-${columnAccessor}-${processInstance.id}`}
onClick={() => navigateToProcessInstance(processInstance)}
onKeyDown={() => navigateToProcessInstance(processInstance)}
data-qa={`process-instance-show-link-${columnAccessor}`}
@ -512,7 +512,7 @@ export default function ProcessInstanceListTable({
if (hasAccessToCompleteTask && processInstance.task_id) {
goButtonElement = (
<Button
kind="secondary"
kind="primary"
href={taskShowUrl}
style={{ width: '60px' }}
size="sm"

View File

@ -796,7 +796,7 @@ export default function ProcessInstanceListTableWithFilters({
return (
<ProcessInstanceListSaveAsReport
onSuccess={onSaveReportSuccess}
buttonClassName="button-white-background narrow-button"
buttonClassName="narrow-button"
buttonText="Save"
processInstanceReportSelection={processInstanceReportSelection}
reportMetadata={reportMetadata}
@ -1040,6 +1040,7 @@ export default function ProcessInstanceListTableWithFilters({
formElements.push(
<Dropdown
titleText="Display type"
label="Display type"
id="report-column-display-type"
items={[''].concat(Object.values(filterDisplayTypes))}
selectedItem={
@ -1064,6 +1065,7 @@ export default function ProcessInstanceListTableWithFilters({
formElements.push(
<Dropdown
titleText="Operator"
label="Operator"
id="report-column-condition-operator"
items={Object.keys(filterOperatorMappings)}
selectedItem={operator || null}
@ -1110,6 +1112,7 @@ export default function ProcessInstanceListTableWithFilters({
onRequestSubmit={handleUpdateReportColumn}
onRequestClose={handleColumnFormClose}
hasScrollingContent
aria-label={modalHeading}
>
{formElements}
</Modal>
@ -1202,6 +1205,7 @@ export default function ProcessInstanceListTableWithFilters({
<Dropdown
id="system-report-dropdown"
titleText="System report"
label="System report"
items={['', ...systemReportOptions]}
itemToString={(item: any) => titleizeString(item)}
selectedItem={systemReport}
@ -1221,6 +1225,7 @@ export default function ProcessInstanceListTableWithFilters({
<Dropdown
id="user-group-dropdown"
titleText="Assigned user group"
label="Assigned user group"
items={['', ...userGroups]}
itemToString={(item: any) => item}
selectedItem={selectedUserGroup}
@ -1282,6 +1287,7 @@ export default function ProcessInstanceListTableWithFilters({
onRequestSubmit={handleAdvancedOptionsClose}
onRequestClose={handleAdvancedOptionsClose}
hasScrollingContent
aria-label="advanced filter options"
size="lg"
>
{formElements}
@ -1457,8 +1463,8 @@ export default function ProcessInstanceListTableWithFilters({
<Column sm={4} md={4} lg={8}>
<ButtonSet>
<Button
kind=""
className="button-white-background narrow-button"
kind="tertiary"
className="narrow-button"
onClick={clearFilters}
>
Clear

View File

@ -499,16 +499,16 @@ export default function ProcessInstanceLogList({
<Column sm={4} md={4} lg={8}>
<ButtonSet>
<Button
kind=""
className="button-white-background narrow-button"
kind="tertiary"
className="narrow-button"
onClick={resetFiltersAndRun}
>
Reset
</Button>
{shouldDisplayClearButton && (
<Button
kind=""
className="button-white-background narrow-button"
kind="tertiary"
className="narrow-button"
onClick={clearFilters}
>
Clear

View File

@ -333,8 +333,7 @@ export default function ProcessModelForm({
<Button
data-qa="add-notification-address-button"
renderIcon={AddAlt}
className="button-white-background"
kind=""
kind="tertiary"
size="sm"
onClick={() => {
addBlankNotificationAddress();
@ -365,8 +364,7 @@ export default function ProcessModelForm({
<Button
data-qa="add-metadata-extraction-path-button"
renderIcon={AddAlt}
className="button-white-background"
kind=""
kind="tertiary"
size="sm"
onClick={() => {
addBlankMetadataExtractionPath();
@ -383,7 +381,7 @@ export default function ProcessModelForm({
const formButtons = () => {
return (
<Button kind="secondary" type="submit">
<Button kind="primary" type="submit">
Submit
</Button>
);

View File

@ -155,18 +155,13 @@ export default function ReactDiagramEditor({
if (diagramType === 'dmn') {
modeler = (diagramModelerState as any).getActiveViewer();
}
try {
if (modeler) {
if (amount === 0) {
const canvas = modeler.get('canvas');
canvas.zoom(FitViewport, 'auto');
} else {
modeler.get('zoomScroll').stepZoom(amount);
}
} catch (e) {
console.error(
'zoom failed, certain modes in DMN do not support zooming.',
e
);
}
}
},
@ -595,7 +590,7 @@ export default function ReactDiagramEditor({
if (diagramType === 'dmn') {
newDiagramFileName = 'new_dmn_diagram.dmn';
}
fetchDiagramFromURL(`${process.env.PUBLIC_URL}/${newDiagramFileName}`);
fetchDiagramFromURL(`/${newDiagramFileName}`);
return undefined;
}

View File

@ -8,53 +8,77 @@ import {
Button,
} from '@carbon/react';
import { JsonSchemaExample } from '../../interfaces';
import textSchema from '../../resources/json_schema_examples/text-schema.json';
import textUiSchema from '../../resources/json_schema_examples/text-uischema.json';
import textareaSchema from '../../resources/json_schema_examples/textarea-schema.json';
import textareaUiSchema from '../../resources/json_schema_examples/textarea-uischema.json';
import dateSchema from '../../resources/json_schema_examples/date-schema.json';
import dateUiSchema from '../../resources/json_schema_examples/date-uischema.json';
import choiceSchema from '../../resources/json_schema_examples/multiple-choice-schema.json';
import choiceUiSchema from '../../resources/json_schema_examples/multiple-choice-uischema.json';
import passwordSchema from '../../resources/json_schema_examples/password-schema.json';
import passwordUiSchema from '../../resources/json_schema_examples/password-uischema.json';
import typeaheadSchema from '../../resources/json_schema_examples/typeahead-schema.json';
import typeaheadUiSchema from '../../resources/json_schema_examples/typeahead-uischema.json';
import checkboxSchema from '../../resources/json_schema_examples/checkbox-schema.json';
import dropdownSchema from '../../resources/json_schema_examples/dropdown-schema.json';
import dropdownData from '../../resources/json_schema_examples/dropdown-exampledata.json';
import nestedSchema from '../../resources/json_schema_examples/nested-schema.json';
const examples: JsonSchemaExample[] = [];
examples.push({
schema: require('../../resources/json_schema_examples/text-schema.json'), // eslint-disable-line global-require
ui: require('../../resources/json_schema_examples/text-uischema.json'), // eslint-disable-line global-require
examples.push(
{
schema: textSchema,
ui: textUiSchema,
data: {},
});
examples.push({
schema: require('../../resources/json_schema_examples/textarea-schema.json'), // eslint-disable-line global-require
ui: require('../../resources/json_schema_examples/textarea-uischema.json'), // eslint-disable-line global-require
},
{
schema: textareaSchema,
ui: textareaUiSchema,
data: {},
});
examples.push({
schema: require('../../resources/json_schema_examples/checkbox-schema.json'), // eslint-disable-line global-require
},
{
schema: checkboxSchema,
ui: {},
data: {},
});
examples.push({
schema: require('../../resources/json_schema_examples/date-schema.json'), // eslint-disable-line global-require
ui: require('../../resources/json_schema_examples/date-uischema.json'), // eslint-disable-line global-require
},
{
schema: dateSchema,
ui: dateUiSchema,
data: {},
});
examples.push({
schema: require('../../resources/json_schema_examples/dropdown-schema.json'), // eslint-disable-line global-require
},
{
schema: dropdownSchema,
ui: {},
data: require('../../resources/json_schema_examples/dropdown-exampledata.json'), // eslint-disable-line global-require
});
examples.push({
schema: require('../../resources/json_schema_examples/multiple-choice-schema.json'), // eslint-disable-line global-require
ui: require('../../resources/json_schema_examples/multiple-choice-uischema.json'), // eslint-disable-line global-require
data: dropdownData,
},
{
schema: choiceSchema,
ui: choiceUiSchema,
data: {},
});
examples.push({
schema: require('../../resources/json_schema_examples/password-schema.json'), // eslint-disable-line global-require
ui: require('../../resources/json_schema_examples/password-uischema.json'), // eslint-disable-line global-require
},
{
schema: passwordSchema,
ui: passwordUiSchema,
data: {},
});
examples.push({
schema: require('../../resources/json_schema_examples/nested-schema.json'), // eslint-disable-line global-require
},
{
schema: nestedSchema,
ui: {},
data: {},
});
examples.push({
schema: require('../../resources/json_schema_examples/typeahead-schema.json'), // eslint-disable-line global-require
ui: require('../../resources/json_schema_examples/typeahead-uischema.json'), // eslint-disable-line global-require
},
{
schema: typeaheadSchema,
ui: typeaheadUiSchema,
data: {},
});
}
);
type OwnProps = {
onSelect: Function;
@ -72,11 +96,7 @@ export default function ExamplesTable({ onSelect }: OwnProps) {
<td>{example.schema.title}</td>
<td>{example.schema.description}</td>
<td>
<Button
kind="secondary"
size="sm"
onClick={() => selectExample(index)}
>
<Button kind="primary" size="sm" onClick={() => selectExample(index)}>
Load
</Button>
</td>

View File

@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { Tabs, TabList, Tab } from '@carbon/react';
import { SpiffTab } from '../interfaces';
import SpiffTooltip from './SpiffTooltip';
type OwnProps = {
tabs: SpiffTab[];
@ -10,7 +9,7 @@ type OwnProps = {
export default function SpiffTabs({ tabs }: OwnProps) {
const location = useLocation();
const [selectedTabIndex, setSelectedTabIndex] = useState<number>(0);
const [selectedTabIndex, setSelectedTabIndex] = useState<number | null>(null);
const navigate = useNavigate();
useEffect(() => {
@ -25,14 +24,11 @@ export default function SpiffTabs({ tabs }: OwnProps) {
const tabComponents = tabs.map((spiffTab: SpiffTab) => {
return (
<SpiffTooltip title={spiffTab?.tooltip}>
<Tab onClick={() => navigate(spiffTab.path)}>
{spiffTab.display_name}
</Tab>
</SpiffTooltip>
<Tab onClick={() => navigate(spiffTab.path)}>{spiffTab.display_name}</Tab>
);
});
if (selectedTabIndex !== null && tabComponents.length > selectedTabIndex) {
return (
<>
<Tabs selectedIndex={selectedTabIndex}>
@ -41,4 +37,6 @@ export default function SpiffTabs({ tabs }: OwnProps) {
<br />
</>
);
}
return null;
}

View File

@ -1,3 +1,5 @@
declare const window: Window & typeof globalThis;
const { port, hostname } = window.location;
let protocol = 'https';
@ -56,23 +58,16 @@ if (!backendBaseUrl) {
}
backendBaseUrl = `${protocol}://${hostAndPortAndPathPrefix}/v1.0`;
// this can only ever work locally since this is a static site.
// use spiffworkflowFrontendJsenv if you want to inject env vars
// that can be read by the static site.
if (process.env.REACT_APP_BACKEND_BASE_URL) {
backendBaseUrl = process.env.REACT_APP_BACKEND_BASE_URL;
}
}
if (!backendBaseUrl.endsWith('/v1.0')) {
backendBaseUrl += '/v1.0';
}
export const BACKEND_BASE_URL = backendBaseUrl;
export const DOCUMENTATION_URL = documentationUrl;
const BACKEND_BASE_URL = backendBaseUrl;
const DOCUMENTATION_URL = documentationUrl;
export const PROCESS_STATUSES = [
const PROCESS_STATUSES = [
'complete',
'error',
'not_started',
@ -122,11 +117,23 @@ const carbonDateFormat = generalDateFormat
.replace(/\bMMM\b/, 'M')
.replace(/\bMMMM\b/, 'F')
.replace(/\bdd\b/, 'd');
export const DATE_TIME_FORMAT = `${generalDateFormat} HH:mm:ss`;
export const TIME_FORMAT_HOURS_MINUTES = 'HH:mm';
export const DATE_FORMAT = generalDateFormat;
export const DATE_FORMAT_CARBON = carbonDateFormat;
export const DATE_FORMAT_FOR_DISPLAY = generalDateFormat.toLowerCase();
export const DATE_RANGE_DELIMITER = ':::';
const DATE_TIME_FORMAT = `${generalDateFormat} HH:mm:ss`;
const TIME_FORMAT_HOURS_MINUTES = 'HH:mm';
const DATE_FORMAT = generalDateFormat;
const DATE_FORMAT_CARBON = carbonDateFormat;
const DATE_FORMAT_FOR_DISPLAY = generalDateFormat.toLowerCase();
const DATE_RANGE_DELIMITER = ':::';
export const SPIFF_ENVIRONMENT = spiffEnvironment;
const SPIFF_ENVIRONMENT = spiffEnvironment;
export {
DATE_TIME_FORMAT,
TIME_FORMAT_HOURS_MINUTES,
DATE_FORMAT,
DATE_FORMAT_CARBON,
DATE_FORMAT_FOR_DISPLAY,
DATE_RANGE_DELIMITER,
BACKEND_BASE_URL,
DOCUMENTATION_URL,
PROCESS_STATUSES,
SPIFF_ENVIRONMENT,
};

View File

@ -10,7 +10,7 @@ const appVersionInfo = () => {
versionInfoFromHtmlMetaTag.getAttribute('content');
if (
versionInfoContentString &&
versionInfoContentString !== '%REACT_APP_VERSION_INFO%'
versionInfoContentString !== '%VITE_VERSION_INFO%'
) {
versionInfo = JSON.parse(versionInfoContentString);
}

View File

@ -144,17 +144,69 @@ h3 {
* so they match our normal scheme better
*/
.cds--btn--tertiary:focus {
color: white;
color: #161616;
border-color: #161616;
background-color: #f4f4f4;
}
.cds--btn--tertiary:hover {
color: #161616;
border-color: #161616;
background-color: #f4f4f4;
}
.cds--btn--tertiary:visited:hover {
color: #161616;
border-color: #161616;
background-color: #f4f4f4;
}
.cds--btn--secondary {
color: #161616;
border-color: #efefef;
background-color: #efefef;
}
.cds--btn--secondary:visited {
color: #161616;
border-color: #efefef;
background-color: #efefef;
}
.cds--btn--secondary:focus {
color: #161616;
border-color: #efefef;
background-color: #efefef;
}
.cds--btn--secondary:hover {
color: #161616;
border-color: #dddddd;
background-color: #dddddd;
}
.cds--btn--secondary:visited:hover {
color: #161616;
border-color: #dddddd;
background-color: #dddddd;
}
.cds--modal-footer .cds--btn--secondary {
color: #ffffff;
border-color: #393939;
background-color: #393939;
}
.cds--btn--tertiary:hover {
color: white;
.cds--modal-footer .cds--btn--secondary:visited {
color: #ffffff;
border-color: #393939;
background-color: #393939;
}
.cds--modal-footer .cds--btn--secondary:focus {
color: #ffffff;
border-color: #393939;
background-color: #393939;
}
.cds--modal-footer .cds--btn--secondary:hover {
color: #ffffff;
border-color: #474747;
background-color: #474747;
}
.cds--btn--tertiary:visited:hover {
color: white;
.cds--modal-footer .cds--btn--secondary:visited:hover {
color: #ffffff;
border-color: #474747;
background-color: #474747;
}

View File

@ -8,7 +8,9 @@
// )
// );
@use '@carbon/react';
@use '@carbon/react' with (
$font-path: '@ibm/plex'
);
@use '@carbon/styles';
// @include grid.flex-grid();

View File

@ -146,6 +146,7 @@ export default function BaseInputTemplate<
<TextInput
id={id}
className="text-input"
labelText=""
helperText={commonAttributes.helperText}
invalid={commonAttributes.invalid}
invalidText={commonAttributes.errorMessageForField}

View File

@ -1,5 +1,4 @@
import { useEffect, useState } from 'react';
// @ts-ignore
import { Table } from '@carbon/react';
import { AuthenticationItem } from '../interfaces';
import HttpService from '../services/HttpService';
@ -92,10 +91,10 @@ export default function AuthenticationList() {
return (
<>
{buildTable()}
{AuthenticationConfiguration()}
<AuthenticationConfiguration />
</>
);
}
return <main />;
return null;
}

View File

@ -27,6 +27,7 @@ export default function BaseRoutes({ extensionUxElements }: OwnProps) {
return (
<Route
path={uxElement.page}
key={uxElement.page}
element={<Extension pageIdentifier={uxElement.page} />}
/>
);

View File

@ -74,7 +74,7 @@ export default function ProcessGroupList() {
<ProcessBreadcrumb hotCrumbs={[['Process Groups']]} />
<Stack orientation="horizontal" gap={3}>
<Can I="POST" a={targetUris.processGroupListPath} ability={ability}>
<Button kind="secondary" href="/process-groups/new">
<Button kind="primary" href="/process-groups/new">
Add a process group
</Button>
</Can>

View File

@ -915,7 +915,7 @@ export default function ProcessModelEditDiagram() {
)}
<Button
className="m-top-10"
kind="secondary"
kind="primary"
onClick={() => handleProcessScriptAssist()}
disabled={scriptAssistLoading}
>

View File

@ -64,7 +64,7 @@ export default function ProcessModelNewExperimental() {
setProcessModelDescriptiveText(event.target.value)
}
/>
<Button kind="secondary" type="submit">
<Button kind="primary" type="submit">
Submit
</Button>
</Form>

View File

@ -339,7 +339,7 @@ export default function ProcessModelShow() {
let actionsTableCell = null;
if (processModelFile.name.match(/\.(dmn|bpmn|json|md)$/)) {
actionsTableCell = (
<TableCell key={`${processModelFile.name}-cell`} align="right">
<TableCell key={`${processModelFile.name}-action`} align="right">
{renderButtonElements(processModelFile, isPrimaryBpmnFile)}
</TableCell>
);

View File

@ -71,11 +71,7 @@ export default function SecretNew() {
}}
/>
<ButtonSet>
<Button
kind=""
className="button-white-background"
onClick={navigateToSecrets}
>
<Button kind="tertiary" onClick={navigateToSecrets}>
Cancel
</Button>
<Button kind="primary" type="submit">

View File

@ -159,6 +159,7 @@ export default function SecretShow() {
<TextInput
id="secret_value"
name="secret_value"
labelText="Secret value"
value={secret.value}
onChange={handleSecretValueChange}
disabled={

View File

@ -360,7 +360,7 @@ export default function TaskShow() {
id="close-button"
onClick={handleCloseButton}
disabled={formButtonsDisabled}
kind="secondary"
kind="primary"
title="Save data as draft and close the form."
>
Save and close

View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1 @@
import '@testing-library/jest-dom';

View File

@ -3,11 +3,18 @@
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react-jsx",
"module": "commonjs",
"skipLibCheck": true,
"strict": true,
"target": "es2021",
"resolveJsonModule": true,
"lib": ["dom", "dom.iterable", "esnext"],
"types": ["vite/client", "node", "vitest/globals"],
"module": "esnext",
"target": "ESNext",
"moduleResolution": "Node",
"isolatedModules": true
},
"include": ["src/**/*"]
"include": ["src", "test/vitest.setup.ts"]
}

View File

@ -0,0 +1,42 @@
import preact from '@preact/preset-vite';
import prefresh from '@prefresh/vite';
import { defineConfig } from 'vite';
// import react from '@vitejs/plugin-react';
import viteTsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
// depending on your application, base can also be "/"
base: '/',
plugins: [
// react(),
// seems to replace preact. hot module replacement doesn't work, so commented out. also causes errors when navigating with TabList:
// Cannot read properties of undefined (reading 'disabled')
// prefresh(),
// we need preact for bpmn-js-spiffworkflow. see https://forum.bpmn.io/t/custom-prop-for-service-tasks-typeerror-cannot-add-property-object-is-not-extensible/8487
preact(),
viteTsconfigPaths(),
],
// for prefresh, from https://github.com/preactjs/prefresh/issues/454#issuecomment-1456491801, not working
// optimizeDeps: {
// include: ['preact/hooks', 'preact/compat', 'preact']
// },
server: {
// this ensures that the browser DOES NOT open upon server start
open: false,
port: 7001,
},
preview: {
port: 7001,
},
resolve: {
alias: {
inferno:
process.env.NODE_ENV !== 'production'
? 'inferno/dist/index.dev.esm.js'
: 'inferno/dist/index.esm.js',
},
preserveSymlinks: true,
},
});

View File

@ -0,0 +1,11 @@
import { defineConfig } from 'vite';
// https://vitejs.dev/config/
export default defineConfig({
test: {
include: ['./src/**/*.test.ts', './src/**/*.test.tsx'],
setupFiles: ['test/vitest.setup.ts'],
globals: true,
environment: 'jsdom',
},
});