Squashed 'spiffworkflow-frontend/' changes from c096ca3f9..326040b3c

326040b3c Frontend label changes (#54)
b4785077a added some more widget types to the form builder. w/ burnettk
27333e941 added basic form builder. w/ burnettk
f2e9ad2d6 pass the the correct model to id on create
92f6366a0 removed tabs from taskshow page w/ burnettk cullerton
bb146410f fixed some of the form widgets w/ burnettk cullerton
00db85342 Merge branch 'main' of github.com:sartography/spiff-arena
084069c72 added create new instance page and moved model run to a component w/ burnettk cullerton
4ba973b55 Merge branch 'main' into update-workflow-json
83b795a4e fixed eslint issues w/ burnettk
44920037c ran some pyl w/ burnettk cullerton
3c8ba0e89 load diagram page even if processes are not set w/ burnettk cullerton
4cdea6bc8 moved model delete button to show page as an icon w/ burnettk
aa2b5b9ff moved group list tiles to component and use on group show page w/ burnettk
61366c3d6 display groups as tiles on list page w/ burnettk
0282dde54 add simple refresh capability
482e6f425 upgrades
ec3019706 hide messages and configuration if not authorized w/ burnettk
ef3a5ff21 default time to midnight if it is null when filtering w/ burnettk
697c069c1 Merge pull request #48 from sartography/feature/add_times_to_instance_filter
c4daaeb1c added back the useEffect for report filtering w/ burnettk
a01f9db11 times on instance list table are working now w/ burnettk
38f1b58fc attempting to use date objects as the date states w/ burnettk
f430fa274 oops, just for one column
6ee14c93c just use the word id in the header and add tooltip
5dbcda2d3 a little cleanup to forms w/ burnettk
50429bc3d remove br since it felt like too much margin
f18435dd0 revert to working mui radio
6c3e59d65 notes about validation issue
abf6caafd add inline errors
7bf2b0673 Merge pull request #45 from sartography/feature/form_carbon_theme
6b9e77711 fixed error messaging a little bit for forms w/ burnettk cullerton
e3ba944f0 Merge pull request #43 from sartography/send_filters
eda32f7a8 Searching for call activities seems to be working now. I had the clear_caches in the wrong place previously - fixing.
cc52a5577 Pre-pr cleanup
e0af333d3 lint
c7e4feab1 Clear/remove filter works
2346a5d34 some updates for the carbon form theme w/ burnettk cullerton
8e4bedef3 added eslintignore file to ignore carbon theme for now w/ burnettk cullerton
1a63f312a added radio buttons w/ burnettk cullerton
5cf910d55 Minor tweak, in the hopes of getting a text box to update correctly.
035ab5600 Add flag to indicate if user filtered
9449bfce7 specify onRequestClose on modals w/ burnettk cullerton
951c53efd updated a couple form components to work with carbon w/ burnettk cullerton
341731487 support ts and js for lint and format commands w/ burnettk cullerton
ab089da90 copied mui theme to use as base for carbon theme w/ burnettk cullerton
c338e35f1 theme working with mui from the internet w/ burnettk cullerton
925790131 Merge branch 'main' of github.com:sartography/spiff-arena into send_filters
f390f82be Set process model from filter
d76bb34df attempting to add a theme w/ burnettk cullerton
18ccdbbcf added lint fix for frontend to pyl w/ burnettk cullerton
20b3c0c7b added development permission for test user w/ burnettk cullerton
8714a0ae2 Set status from filters
a684a71fc added some permissions for tasks
f6c526ca7 Set date filters from response
0d198c73a docs
35cdb2f33 add cypress grep
c57ae7772 added some permissions to the process model show page w/ burnettk
b5d2109b8 use id_for_file_path when using the process model id as a path for windows and added some more permission stuff to the frontend w/ burnettk
2ce445fa3 added permission service to frontend to allow checking for permissions w/ burnettk
225755493 fixed linting issues w/ burnettk
4d6fc0a82 added configuration nav item to help reduce nav items w/ burnettk
f0a5b4142 Navigate to my task (#35)
fc2a3d451 Merge pull request #36 from sartography/feature/call_activity_selection
ed9ec1547 Merge remote-tracking branch 'origin/main' into feature/call_activity_selection
56adc8118 some minor updates to model show page w/ burnettk
31d3b947d more refactoring for process instance list w/ burnettk
a4108a25e turned the table list route into a table component w/ burnettk
75a198995 refactored pagination table to allow prefixing page options w/ burnettk
14414079a Mostly a name change from BpmnProcessIdLookup to SpecReferenceCache.  I landed on this unfortunate name because:
902c3b7e3 added remaining task tables w/ burnettk
c4e2cf460 added message correlations to message instance list api call w/ burnettk
8866d6d00 i think this is not always truthy, eslint
fc5d8bc8e open accordion by default per feedback
19942b878 dedup
c822ecea6 fix a couple tests
3d2578b7a Merge remote-tracking branch 'origin/main' into feature/home_page_redesign
af8f245ce lint
43dabb65f add the username to the task list w/ burnettk
6d598978b added more task tables w/ burnettk
f4edf3754 added tasks for my open processes page w/ burnettk
1e063bf06 some more task tab play
d0e2e3b64 Merge remote-tracking branch 'origin/main' into feature/home_page_redesign
968acbc99 added home page routes and some tab stuff w/ burnettk

git-subtree-dir: spiffworkflow-frontend
git-subtree-split: 326040b3cb9cd16faeea545a25a91ed41f4340cf
This commit is contained in:
burnettk 2022-11-20 19:57:17 -05:00
parent e10bc47851
commit b198341b00
113 changed files with 6261 additions and 1416 deletions

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
/src/themes/carbon

View File

@ -36,6 +36,7 @@ module.exports = {
],
'react/react-in-jsx-scope': 'off',
'react/require-default-props': 'off',
'import/prefer-default-export': 'off',
'no-unused-vars': [
'error',
{

View File

@ -7,6 +7,9 @@ function error_handler() {
trap 'error_handler ${LINENO} $?' ERR
set -o errtrace -o errexit -o nounset -o pipefail
# see also: npx cypress run --env grep="can filter",grepFilterSpecs=true
# https://github.com/cypress-io/cypress/tree/develop/npm/grep#pre-filter-specs-grepfilterspecs
command="${1:-}"
if [[ -z "$command" ]]; then
command=open

View File

@ -6,8 +6,9 @@ module.exports = defineConfig({
chromeWebSecurity: false,
e2e: {
baseUrl: 'http://localhost:7001',
setupNodeEvents(_on, _config) {
// implement node event listeners here
setupNodeEvents(_on, config) {
require('@cypress/grep/src/plugin')(config);
return config;
},
},

View File

@ -169,14 +169,13 @@ describe('process-instances', () => {
cy.getBySel('process-instance-list-link').click();
cy.assertAtLeastOneItemInPaginatedResults();
const statusSelect = '#process-instance-status-select';
PROCESS_STATUSES.forEach((processStatus) => {
if (!['all', 'waiting'].includes(processStatus)) {
cy.get('#process-instance-status-select').click();
cy.get('#process-instance-status-select')
.contains(processStatus)
.click();
cy.get(statusSelect).click();
cy.get(statusSelect).contains(processStatus).click();
// close the dropdown again
cy.get('#process-instance-status-select').click();
cy.get(statusSelect).click();
cy.getBySel('filter-button').click();
cy.assertAtLeastOneItemInPaginatedResults();
cy.getBySel(`process-instance-status-${processStatus}`).contains(

View File

@ -144,10 +144,11 @@ describe('process-models', () => {
cy.getBySel('process-instance-list-link').click();
cy.getBySel('process-instance-show-link').click();
cy.contains('Delete').click();
cy.getBySel('process-instance-delete').click();
cy.contains('Are you sure');
cy.getBySel('modal-confirmation-dialog').find('.cds--btn--danger').click();
cy.contains(`Process Instances for: ${groupId}/${modelId}`);
// in breadcrumb
cy.contains(modelId).click();
cy.contains('Edit process model').click();

View File

@ -16,5 +16,9 @@
// Import commands.js using ES2015 syntax:
import './commands';
import registerCypressGrep from '@cypress/grep';
registerCypressGrep();
// Alternatively you can use CommonJS syntax:
// require('./commands')

2215
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,9 +9,16 @@
"@carbon/icons-react": "^11.10.0",
"@carbon/react": "^1.16.0",
"@carbon/styles": "^1.16.0",
"@casl/ability": "^6.3.2",
"@casl/react": "^3.1.0",
"@ginkgo-bioworks/react-json-schema-form-builder": "^2.9.0",
"@monaco-editor/react": "^4.4.5",
"@rjsf/core": "^4.2.0",
"@mui/material": "^5.10.14",
"@react-icons/all-files": "^4.1.0",
"@rjsf/core": "*",
"@rjsf/mui": "^5.0.0-beta.13",
"@rjsf/utils": "^5.0.0-beta.13",
"@rjsf/validator-ajv6": "^5.0.0-beta.13",
"@tanstack/react-table": "^8.2.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
@ -71,9 +78,9 @@
"test": "react-scripts test --coverage",
"t": "npm test -- --watchAll=false",
"eject": "craco eject",
"format": "prettier --write src/**/*.js{,x}",
"lint": "./node_modules/.bin/eslint src *.js",
"lint:fix": "./node_modules/.bin/eslint --fix src *.js"
"format": "prettier --write src/**/*.[tj]s{,x}",
"lint": "./node_modules/.bin/eslint src",
"lint:fix": "./node_modules/.bin/eslint --fix src"
},
"eslintConfig": {
"extends": [
@ -94,6 +101,7 @@
]
},
"devDependencies": {
"@cypress/grep": "^3.1.0",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.6",
"cypress": "^10.8.0",

View File

@ -3,15 +3,17 @@ import { useMemo, useState } from 'react';
import { Content } from '@carbon/react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { defineAbility } from '@casl/ability';
import ErrorContext from './contexts/ErrorContext';
import NavigationBar from './components/NavigationBar';
import HomePage from './routes/HomePage';
import TaskShow from './routes/TaskShow';
import HomePageRoutes from './routes/HomePageRoutes';
import ErrorBoundary from './components/ErrorBoundary';
import AdminRoutes from './routes/AdminRoutes';
import { ErrorForDisplay } from './interfaces';
import { AbilityContext } from './contexts/Can';
export default function App() {
const [errorMessage, setErrorMessage] = useState<ErrorForDisplay | null>(
null
@ -22,6 +24,8 @@ export default function App() {
[errorMessage]
);
const ability = defineAbility(() => {});
let errorTag = null;
if (errorMessage) {
let sentryLinkTag = null;
@ -47,29 +51,24 @@ export default function App() {
return (
<div className="cds--white">
<ErrorContext.Provider value={errorContextValueArray}>
<BrowserRouter>
<NavigationBar />
<Content>
{errorTag}
<ErrorBoundary>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/tasks" element={<HomePage />} />
<Route path="/admin/*" element={<AdminRoutes />} />
<Route
path="/tasks/:process_instance_id/:task_id"
element={<TaskShow />}
/>
<Route
path="/tasks/:process_instance_id/:task_id"
element={<TaskShow />}
/>
</Routes>
</ErrorBoundary>
</Content>
</BrowserRouter>
</ErrorContext.Provider>
{/* @ts-ignore */}
<AbilityContext.Provider value={ability}>
<ErrorContext.Provider value={errorContextValueArray}>
<BrowserRouter>
<NavigationBar />
<Content>
{errorTag}
<ErrorBoundary>
<Routes>
<Route path="/*" element={<HomePageRoutes />} />
<Route path="/tasks/*" element={<HomePageRoutes />} />
<Route path="/admin/*" element={<AdminRoutes />} />
</Routes>
</ErrorBoundary>
</Content>
</BrowserRouter>
</ErrorContext.Provider>
</AbilityContext.Provider>
</div>
);
}

View File

@ -3,6 +3,7 @@ import { useState } from 'react';
import { Button, Modal } from '@carbon/react';
type OwnProps = {
'data-qa'?: string;
description?: string;
buttonLabel?: string;
onConfirmation: (..._args: any[]) => any;
@ -18,6 +19,7 @@ export default function ButtonWithConfirmation({
description,
buttonLabel,
onConfirmation,
'data-qa': dataQa,
title = 'Are you sure?',
confirmButtonLabel = 'OK',
kind = 'danger',
@ -51,6 +53,7 @@ export default function ButtonWithConfirmation({
secondaryButtonText="Cancel"
onSecondarySubmit={handleConfirmationPromptCancel}
onRequestSubmit={handleConfirmation}
onRequestClose={handleConfirmationPromptCancel}
/>
);
};
@ -58,6 +61,7 @@ export default function ButtonWithConfirmation({
return (
<>
<Button
data-qa={dataQa}
onClick={handleShowConfirmationPrompt}
kind={kind}
renderIcon={renderIcon}

View File

@ -1,57 +0,0 @@
import React from 'react';
import HttpService from '../services/HttpService';
import { modifyProcessModelPath } from '../helpers';
type Props = {
processGroupId: string;
processModelId: string;
onUploadedCallback?: (..._args: any[]) => any;
};
export default class FileInput extends React.Component<Props> {
fileInput: any;
processGroupId: any;
processModelId: any;
onUploadedCallback: any;
constructor({ processGroupId, processModelId, onUploadedCallback }: Props) {
super({ processGroupId, processModelId, onUploadedCallback });
this.handleSubmit = this.handleSubmit.bind(this);
this.fileInput = React.createRef();
this.processGroupId = processGroupId;
this.processModelId = processModelId;
this.onUploadedCallback = onUploadedCallback;
}
handleSubmit(event: any) {
event.preventDefault();
const modifiedProcessModelId = modifyProcessModelPath(
`${this.processGroupId}/${this.processModelId}`
);
const url = `/process-models/${modifiedProcessModelId}/files`;
const formData = new FormData();
formData.append('file', this.fileInput.current.files[0]);
formData.append('fileName', this.fileInput.current.files[0].name);
HttpService.makeCallToBackend({
path: url,
successCallback: this.onUploadedCallback,
httpMethod: 'POST',
postBody: formData,
});
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Upload file:
<input type="file" ref={this.fileInput} />
</label>
<button type="submit">Submit</button>
</form>
);
}
}

View File

@ -0,0 +1,13 @@
import ProcessInstanceListTable from './ProcessInstanceListTable';
const paginationQueryParamPrefix = 'my_completed_instances';
export default function MyCompletedInstances() {
return (
<ProcessInstanceListTable
filtersEnabled={false}
paginationQueryParamPrefix={paginationQueryParamPrefix}
perPageOptions={[2, 5, 25]}
/>
);
}

View File

@ -17,9 +17,13 @@ import {
import { Logout, Login } from '@carbon/icons-react';
import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Can } from '@casl/react';
// @ts-expect-error TS(2307) FIXME: Cannot find module '../logo.svg' or its correspond... Remove this comment to see the full error message
import logo from '../logo.svg';
import UserService from '../services/UserService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
// for ref: https://react-bootstrap.github.io/components/navbar/
export default function NavigationBar() {
@ -34,6 +38,14 @@ export default function NavigationBar() {
const location = useLocation();
const [activeKey, setActiveKey] = useState<string>('');
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.authenticationListPath]: ['GET'],
[targetUris.messageInstanceListPath]: ['GET'],
[targetUris.secretListPath]: ['GET'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
useEffect(() => {
let newActiveKey = '/admin/process-groups';
if (location.pathname.match(/^\/admin\/messages\b/)) {
@ -44,10 +56,8 @@ export default function NavigationBar() {
newActiveKey = '/admin/process-instances/reports';
} else if (location.pathname.match(/^\/admin\/process-instances\b/)) {
newActiveKey = '/admin/process-instances';
} else if (location.pathname.match(/^\/admin\/secrets\b/)) {
newActiveKey = '/admin/secrets';
} else if (location.pathname.match(/^\/admin\/authentications\b/)) {
newActiveKey = '/admin/authentications';
} else if (location.pathname.match(/^\/admin\/configuration\b/)) {
newActiveKey = '/admin/configuration';
} else if (location.pathname === '/') {
newActiveKey = '/';
} else if (location.pathname.match(/^\/tasks\b/)) {
@ -86,6 +96,42 @@ export default function NavigationBar() {
);
};
const configurationElement = () => {
return (
<Can
I="GET"
a={targetUris.authenticationListPath}
ability={ability}
passThrough
>
{(authenticationAllowed: boolean) => {
return (
<Can
I="GET"
a={targetUris.secretListPath}
ability={ability}
passThrough
>
{(secretAllowed: boolean) => {
if (secretAllowed || authenticationAllowed) {
return (
<HeaderMenuItem
href="/admin/configuration"
isCurrentPage={isActivePage('/admin/configuration')}
>
Configuration
</HeaderMenuItem>
);
}
return null;
}}
</Can>
);
}}
</Can>
);
};
const headerMenuItems = () => {
return (
<>
@ -105,35 +151,26 @@ export default function NavigationBar() {
>
Process Instances
</HeaderMenuItem>
<HeaderMenuItem
href="/admin/messages"
isCurrentPage={isActivePage('/admin/messages')}
>
Messages
</HeaderMenuItem>
<HeaderMenuItem
href="/admin/secrets"
isCurrentPage={isActivePage('/admin/secrets')}
>
Secrets
</HeaderMenuItem>
<HeaderMenuItem
href="/admin/authentications"
isCurrentPage={isActivePage('/admin/authentications')}
>
Authentications
</HeaderMenuItem>
<Can I="GET" a={targetUris.messageInstanceListPath} ability={ability}>
<HeaderMenuItem
href="/admin/messages"
isCurrentPage={isActivePage('/admin/messages')}
>
Messages
</HeaderMenuItem>
</Can>
{configurationElement()}
<HeaderMenuItem
href="/admin/process-instances/reports"
isCurrentPage={isActivePage('/admin/process-instances/reports')}
>
Reports
Perspectives
</HeaderMenuItem>
</>
);
};
if (activeKey) {
if (activeKey && ability) {
return (
<HeaderContainer
render={({ isSideNavExpanded, onClickSideNavExpand }: any) => (

View File

@ -1,4 +1,4 @@
import { useNavigate } from 'react-router-dom';
import { useSearchParams } from 'react-router-dom';
// @ts-ignore
import { Pagination } from '@carbon/react';
@ -13,8 +13,7 @@ type OwnProps = {
perPageOptions?: number[];
pagination: PaginationObject | null;
tableToDisplay: any;
queryParamString?: string;
path: string;
paginationQueryParamPrefix?: string;
};
export default function PaginationForTable({
@ -23,16 +22,21 @@ export default function PaginationForTable({
perPageOptions,
pagination,
tableToDisplay,
queryParamString = '',
path,
paginationQueryParamPrefix,
}: OwnProps) {
const PER_PAGE_OPTIONS = [2, 10, 50, 100];
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const paginationQueryParamPrefixToUse = paginationQueryParamPrefix
? `${paginationQueryParamPrefix}_`
: '';
const updateRows = (args: any) => {
const newPage = args.page;
const { pageSize } = args;
navigate(`${path}?page=${newPage}&per_page=${pageSize}${queryParamString}`);
searchParams.set(`${paginationQueryParamPrefixToUse}page`, newPage);
searchParams.set(`${paginationQueryParamPrefixToUse}per_page`, pageSize);
setSearchParams(searchParams);
};
if (pagination) {

View File

@ -165,11 +165,7 @@ export default function ProcessGroupForm({
};
const formButtons = () => {
const buttons = [
<Button kind="secondary" type="submit">
Submit
</Button>,
];
const buttons = [<Button type="submit">Submit</Button>];
if (mode === 'edit') {
buttons.push(
<ButtonWithConfirmation

View File

@ -0,0 +1,95 @@
import { ReactElement, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import {
ArrowRight,
// @ts-ignore
} from '@carbon/icons-react';
import {
ClickableTile,
// @ts-ignore
} from '@carbon/react';
import HttpService from '../services/HttpService';
import { ProcessGroup } from '../interfaces';
import { modifyProcessModelPath, truncateString } from '../helpers';
type OwnProps = {
processGroup?: ProcessGroup;
headerElement?: ReactElement;
};
export default function ProcessGroupListTiles({
processGroup,
headerElement,
}: OwnProps) {
const [searchParams] = useSearchParams();
const [processGroups, setProcessGroups] = useState<ProcessGroup[] | null>(
null
);
useEffect(() => {
const setProcessGroupsFromResult = (result: any) => {
setProcessGroups(result.results);
};
let queryParams = '?per_page=1000';
if (processGroup) {
queryParams = `${queryParams}&process_group_identifier=${processGroup.id}`;
}
HttpService.makeCallToBackend({
path: `/process-groups${queryParams}`,
successCallback: setProcessGroupsFromResult,
});
}, [searchParams, processGroup]);
const processGroupDirectChildrenCount = (pg: ProcessGroup) => {
return (pg.process_models || []).length + (pg.process_groups || []).length;
};
const processGroupsDisplayArea = () => {
let displayText = null;
if (processGroups && processGroups.length > 0) {
displayText = (processGroups || []).map((row: ProcessGroup) => {
return (
<ClickableTile
id="tile-1"
className="tile-process-group"
href={`/admin/process-groups/${modifyProcessModelPath(row.id)}`}
>
<div className="tile-process-group-content-container">
<ArrowRight />
<div className="tile-process-group-display-name">
{row.display_name}
</div>
<p className="tile-description">
{truncateString(row.description || '', 25)}
</p>
<p className="tile-process-group-children-count tile-pin-bottom">
Total Sub Items: {processGroupDirectChildrenCount(row)}
</p>
</div>
</ClickableTile>
);
});
} else {
displayText = <p>No Groups To Display</p>;
}
return displayText;
};
const processGroupArea = () => {
if (processGroups && (!processGroup || processGroups.length > 0)) {
return (
<>
{headerElement}
{processGroupsDisplayArea()}
</>
);
}
return null;
};
if (processGroups) {
return <>{processGroupArea()}</>;
}
return null;
}

View File

@ -0,0 +1,723 @@
import { useContext, useEffect, useMemo, useState } from 'react';
import {
Link,
useNavigate,
useParams,
useSearchParams,
} from 'react-router-dom';
// @ts-ignore
import { Filter } from '@carbon/icons-react';
import {
Button,
ButtonSet,
DatePicker,
DatePickerInput,
Table,
Grid,
Column,
MultiSelect,
TableHeader,
TableHead,
TableRow,
TimePicker,
// @ts-ignore
} from '@carbon/react';
import { PROCESS_STATUSES, DATE_FORMAT, DATE_FORMAT_CARBON } from '../config';
import {
convertDateAndTimeStringsToSeconds,
convertDateObjectToFormattedHoursMinutes,
convertSecondsToFormattedDateString,
convertSecondsToFormattedDateTime,
convertSecondsToFormattedTimeHoursMinutes,
getPageInfoFromSearchParams,
getProcessModelFullIdentifierFromSearchParams,
modifyProcessModelPath,
} from '../helpers';
import PaginationForTable from './PaginationForTable';
import 'react-datepicker/dist/react-datepicker.css';
import ErrorContext from '../contexts/ErrorContext';
import HttpService from '../services/HttpService';
import 'react-bootstrap-typeahead/css/Typeahead.css';
import 'react-bootstrap-typeahead/css/Typeahead.bs5.css';
import { PaginationObject, ProcessModel } from '../interfaces';
import ProcessModelSearch from './ProcessModelSearch';
type OwnProps = {
filtersEnabled?: boolean;
processModelFullIdentifier?: string;
paginationQueryParamPrefix?: string;
perPageOptions?: number[];
};
interface dateParameters {
[key: string]: ((..._args: any[]) => any)[];
}
export default function ProcessInstanceListTable({
filtersEnabled = true,
processModelFullIdentifier,
paginationQueryParamPrefix,
perPageOptions,
}: OwnProps) {
const params = useParams();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [processInstances, setProcessInstances] = useState([]);
const [reportMetadata, setReportMetadata] = useState({});
const [pagination, setPagination] = useState<PaginationObject | null>(null);
const [processInstanceFilters, setProcessInstanceFilters] = useState({});
const oneHourInSeconds = 3600;
const oneMonthInSeconds = oneHourInSeconds * 24 * 30;
const [startFromDate, setStartFromDate] = useState<string>('');
const [startToDate, setStartToDate] = useState<string>('');
const [endFromDate, setEndFromDate] = useState<string>('');
const [endToDate, setEndToDate] = useState<string>('');
const [startFromTime, setStartFromTime] = useState<string>('');
const [startToTime, setStartToTime] = useState<string>('');
const [endFromTime, setEndFromTime] = useState<string>('');
const [endToTime, setEndToTime] = useState<string>('');
const [showFilterOptions, setShowFilterOptions] = useState<boolean>(false);
const [startFromTimeInvalid, setStartFromTimeInvalid] =
useState<boolean>(false);
const [startToTimeInvalid, setStartToTimeInvalid] = useState<boolean>(false);
const [endFromTimeInvalid, setEndFromTimeInvalid] = useState<boolean>(false);
const [endToTimeInvalid, setEndToTimeInvalid] = useState<boolean>(false);
const setErrorMessage = (useContext as any)(ErrorContext)[1];
const [processStatusAllOptions, setProcessStatusAllOptions] = useState<any[]>(
[]
);
const [processStatusSelection, setProcessStatusSelection] = useState<
string[]
>([]);
const [processModelAvailableItems, setProcessModelAvailableItems] = useState<
ProcessModel[]
>([]);
const [processModelSelection, setProcessModelSelection] =
useState<ProcessModel | null>(null);
const dateParametersToAlwaysFilterBy: dateParameters = useMemo(() => {
return {
start_from: [setStartFromDate, setStartFromTime],
start_to: [setStartToDate, setStartToTime],
end_from: [setEndFromDate, setEndFromTime],
end_to: [setEndToDate, setEndToTime],
};
}, [
setStartFromDate,
setStartFromTime,
setStartToDate,
setStartToTime,
setEndFromDate,
setEndFromTime,
setEndToDate,
setEndToTime,
]);
const parametersToGetFromSearchParams = useMemo(() => {
return {
process_model_identifier: null,
process_status: null,
};
}, []);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
function setProcessInstancesFromResult(result: any) {
const processInstancesFromApi = result.results;
setProcessInstances(processInstancesFromApi);
setReportMetadata(result.report_metadata);
setPagination(result.pagination);
setProcessInstanceFilters(result.filters);
}
function getProcessInstances() {
// eslint-disable-next-line prefer-const
let { page, perPage } = getPageInfoFromSearchParams(
searchParams,
undefined,
undefined,
paginationQueryParamPrefix
);
if (perPageOptions && !perPageOptions.includes(perPage)) {
// eslint-disable-next-line prefer-destructuring
perPage = perPageOptions[1];
}
let queryParamString = `per_page=${perPage}&page=${page}`;
const userAppliedFilter = searchParams.get('user_filter');
if (userAppliedFilter) {
queryParamString += `&user_filter=${userAppliedFilter}`;
}
Object.keys(dateParametersToAlwaysFilterBy).forEach(
(paramName: string) => {
const dateFunctionToCall =
dateParametersToAlwaysFilterBy[paramName][0];
const timeFunctionToCall =
dateParametersToAlwaysFilterBy[paramName][1];
const searchParamValue = searchParams.get(paramName);
if (searchParamValue) {
queryParamString += `&${paramName}=${searchParamValue}`;
const dateString = convertSecondsToFormattedDateString(
searchParamValue as any
);
dateFunctionToCall(dateString);
const timeString = convertSecondsToFormattedTimeHoursMinutes(
searchParamValue as any
);
timeFunctionToCall(timeString);
setShowFilterOptions(true);
}
}
);
Object.keys(parametersToGetFromSearchParams).forEach(
(paramName: string) => {
if (
paramName === 'process_model_identifier' &&
processModelFullIdentifier
) {
queryParamString += `&process_model_identifier=${processModelFullIdentifier}`;
} else if (searchParams.get(paramName)) {
// @ts-expect-error TS(7053) FIXME:
const functionToCall = parametersToGetFromSearchParams[paramName];
queryParamString += `&${paramName}=${searchParams.get(paramName)}`;
if (functionToCall !== null) {
functionToCall(searchParams.get(paramName) || '');
}
setShowFilterOptions(true);
}
}
);
HttpService.makeCallToBackend({
path: `/process-instances?${queryParamString}`,
successCallback: setProcessInstancesFromResult,
});
}
function processResultForProcessModels(result: any) {
const processModelFullIdentifierFromSearchParams =
getProcessModelFullIdentifierFromSearchParams(searchParams);
const selectionArray = result.results.map((item: any) => {
const label = `${item.id}`;
Object.assign(item, { label });
if (label === processModelFullIdentifierFromSearchParams) {
setProcessModelSelection(item);
}
return item;
});
setProcessModelAvailableItems(selectionArray);
const processStatusSelectedArray: string[] = [];
const processStatusAllOptionsArray = PROCESS_STATUSES.map(
(processStatusOption: any) => {
const regex = new RegExp(`\\b${processStatusOption}\\b`);
if ((searchParams.get('process_status') || '').match(regex)) {
processStatusSelectedArray.push(processStatusOption);
}
return processStatusOption;
}
);
setProcessStatusSelection(processStatusSelectedArray);
setProcessStatusAllOptions(processStatusAllOptionsArray);
getProcessInstances();
}
if (filtersEnabled) {
// populate process model selection
HttpService.makeCallToBackend({
path: `/process-models?per_page=1000`,
successCallback: processResultForProcessModels,
});
} else {
getProcessInstances();
}
}, [
searchParams,
params,
oneMonthInSeconds,
oneHourInSeconds,
dateParametersToAlwaysFilterBy,
parametersToGetFromSearchParams,
filtersEnabled,
paginationQueryParamPrefix,
processModelFullIdentifier,
perPageOptions,
]);
// This sets the filter data using the saved reports returned from the initial instance_list query.
// This could probably be merged into the main useEffect but it works here now.
useEffect(() => {
const filters = processInstanceFilters as any;
Object.keys(dateParametersToAlwaysFilterBy).forEach((paramName: string) => {
const dateFunctionToCall = dateParametersToAlwaysFilterBy[paramName][0];
const timeFunctionToCall = dateParametersToAlwaysFilterBy[paramName][1];
const paramValue = filters[paramName];
dateFunctionToCall('');
timeFunctionToCall('');
if (paramValue) {
const dateString = convertSecondsToFormattedDateString(
paramValue as any
);
dateFunctionToCall(dateString);
const timeString = convertSecondsToFormattedTimeHoursMinutes(
paramValue as any
);
timeFunctionToCall(timeString);
setShowFilterOptions(true);
}
});
setProcessModelSelection(null);
processModelAvailableItems.forEach((item: any) => {
if (item.id === filters.process_model_identifier) {
setProcessModelSelection(item);
}
});
const processStatusSelectedArray: string[] = [];
if (filters.process_status) {
PROCESS_STATUSES.forEach((processStatusOption: any) => {
const regex = new RegExp(`\\b${processStatusOption}\\b`);
if (filters.process_status.match(regex)) {
processStatusSelectedArray.push(processStatusOption);
}
});
setShowFilterOptions(true);
}
setProcessStatusSelection(processStatusSelectedArray);
}, [
processInstanceFilters,
dateParametersToAlwaysFilterBy,
parametersToGetFromSearchParams,
processModelAvailableItems,
]);
// does the comparison, but also returns false if either argument
// is not truthy and therefore not comparable.
const isTrueComparison = (param1: any, operation: any, param2: any) => {
if (param1 && param2) {
switch (operation) {
case '<':
return param1 < param2;
case '>':
return param1 > param2;
default:
return false;
}
} else {
return false;
}
};
const applyFilter = (event: any) => {
event.preventDefault();
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
undefined,
undefined,
paginationQueryParamPrefix
);
let queryParamString = `per_page=${perPage}&page=${page}&user_filter=true`;
const startFromSeconds = convertDateAndTimeStringsToSeconds(
startFromDate,
startFromTime || '00:00:00'
);
const startToSeconds = convertDateAndTimeStringsToSeconds(
startToDate,
startToTime || '00:00:00'
);
const endFromSeconds = convertDateAndTimeStringsToSeconds(
endFromDate,
endFromTime || '00:00:00'
);
const endToSeconds = convertDateAndTimeStringsToSeconds(
endToDate,
endToTime || '00:00:00'
);
if (isTrueComparison(startFromSeconds, '>', startToSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "start date to"',
});
return;
}
if (isTrueComparison(endFromSeconds, '>', endToSeconds)) {
setErrorMessage({
message: '"End date from" cannot be after "end date to"',
});
return;
}
if (isTrueComparison(startFromSeconds, '>', endFromSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "end date from"',
});
return;
}
if (isTrueComparison(startToSeconds, '>', endToSeconds)) {
setErrorMessage({
message: '"Start date to" cannot be after "end date to"',
});
return;
}
if (startFromSeconds) {
queryParamString += `&start_from=${startFromSeconds}`;
}
if (startToSeconds) {
queryParamString += `&start_to=${startToSeconds}`;
}
if (endFromSeconds) {
queryParamString += `&end_from=${endFromSeconds}`;
}
if (endToSeconds) {
queryParamString += `&end_to=${endToSeconds}`;
}
if (processStatusSelection.length > 0) {
queryParamString += `&process_status=${processStatusSelection}`;
}
if (processModelSelection) {
queryParamString += `&process_model_identifier=${processModelSelection.id}`;
}
setErrorMessage(null);
navigate(`/admin/process-instances?${queryParamString}`);
};
const dateComponent = (
labelString: any,
name: any,
initialDate: any,
initialTime: string,
onChangeDateFunction: any,
onChangeTimeFunction: any,
timeInvalid: boolean,
setTimeInvalid: any
) => {
return (
<>
<DatePicker dateFormat={DATE_FORMAT_CARBON} datePickerType="single">
<DatePickerInput
id={`date-picker-${name}`}
placeholder={DATE_FORMAT}
labelText={labelString}
type="text"
size="md"
autocomplete="off"
allowInput={false}
onChange={(dateChangeEvent: any) => {
if (!initialDate && !initialTime) {
onChangeTimeFunction(
convertDateObjectToFormattedHoursMinutes(new Date())
);
}
onChangeDateFunction(dateChangeEvent.srcElement.value);
}}
value={initialDate}
/>
</DatePicker>
<TimePicker
invalid={timeInvalid}
id="time-picker"
labelText="Select a time"
pattern="^([01]\d|2[0-3]):?([0-5]\d)$"
value={initialTime}
onChange={(event: any) => {
if (event.srcElement.validity.valid) {
setTimeInvalid(false);
} else {
setTimeInvalid(true);
}
onChangeTimeFunction(event.srcElement.value);
}}
/>
</>
);
};
const processStatusSearch = () => {
return (
<MultiSelect
label="Choose Status"
className="our-class"
id="process-instance-status-select"
titleText="Status"
items={processStatusAllOptions}
onChange={(selection: any) => {
setProcessStatusSelection(selection.selectedItems);
}}
itemToString={(item: any) => {
return item || '';
}}
selectionFeedback="top-after-reopen"
selectedItems={processStatusSelection}
/>
);
};
const clearFilters = () => {
setProcessModelSelection(null);
setProcessStatusSelection([]);
setStartFromDate('');
setStartFromTime('');
setStartToDate('');
setStartToTime('');
setEndFromDate('');
setEndFromTime('');
setEndToDate('');
setEndToTime('');
};
const filterOptions = () => {
if (!showFilterOptions) {
return null;
}
return (
<>
<Grid fullWidth className="with-bottom-margin">
<Column md={8}>
<ProcessModelSearch
onChange={(selection: any) =>
setProcessModelSelection(selection.selectedItem)
}
processModels={processModelAvailableItems}
selectedItem={processModelSelection}
/>
</Column>
<Column md={8}>{processStatusSearch()}</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
{dateComponent(
'Start date from',
'start-from',
startFromDate,
startFromTime,
setStartFromDate,
setStartFromTime,
startFromTimeInvalid,
setStartFromTimeInvalid
)}
</Column>
<Column md={4}>
{dateComponent(
'Start date to',
'start-to',
startToDate,
startToTime,
setStartToDate,
setStartToTime,
startToTimeInvalid,
setStartToTimeInvalid
)}
</Column>
<Column md={4}>
{dateComponent(
'End date from',
'end-from',
endFromDate,
endFromTime,
setEndFromDate,
setEndFromTime,
endFromTimeInvalid,
setEndFromTimeInvalid
)}
</Column>
<Column md={4}>
{dateComponent(
'End date to',
'end-to',
endToDate,
endToTime,
setEndToDate,
setEndToTime,
endToTimeInvalid,
setEndToTimeInvalid
)}
</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
<ButtonSet>
<Button
kind=""
className="button-white-background"
onClick={clearFilters}
>
Clear
</Button>
<Button
kind="secondary"
onClick={applyFilter}
data-qa="filter-button"
>
Filter
</Button>
</ButtonSet>
</Column>
</Grid>
</>
);
};
const buildTable = () => {
const headerLabels: Record<string, string> = {
id: 'Id',
process_model_identifier: 'Process Model',
start_in_seconds: 'Start Time',
end_in_seconds: 'End Time',
status: 'Status',
spiff_step: 'SpiffWorkflow Step',
};
const getHeaderLabel = (header: string) => {
return headerLabels[header] ?? header;
};
const headers = (reportMetadata as any).columns.map((column: any) => {
// return <th>{getHeaderLabel((column as any).Header)}</th>;
return getHeaderLabel((column as any).Header);
});
const formatProcessInstanceId = (row: any, id: any) => {
const modifiedProcessModelId: String = modifyProcessModelPath(
row.process_model_identifier
);
return (
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${row.id}`}
>
{id}
</Link>
);
};
const formatProcessModelIdentifier = (_row: any, identifier: any) => {
return (
<Link
to={`/admin/process-models/${modifyProcessModelPath(identifier)}`}
>
{identifier}
</Link>
);
};
const formatSecondsForDisplay = (_row: any, seconds: any) => {
return convertSecondsToFormattedDateTime(seconds) || '-';
};
const defaultFormatter = (_row: any, value: any) => {
return value;
};
const columnFormatters: Record<string, any> = {
id: formatProcessInstanceId,
process_model_identifier: formatProcessModelIdentifier,
start_in_seconds: formatSecondsForDisplay,
end_in_seconds: formatSecondsForDisplay,
};
const formattedColumn = (row: any, column: any) => {
const formatter = columnFormatters[column.accessor] ?? defaultFormatter;
const value = row[column.accessor];
if (column.accessor === 'status') {
return (
<td data-qa={`process-instance-status-${value}`}>
{formatter(row, value)}
</td>
);
}
return <td>{formatter(row, value)}</td>;
};
const rows = processInstances.map((row: any) => {
const currentRow = (reportMetadata as any).columns.map((column: any) => {
return formattedColumn(row, column);
});
return <tr key={row.id}>{currentRow}</tr>;
});
return (
<Table size="lg">
<TableHead>
<TableRow>
{headers.map((header: any) => (
<TableHeader
key={header}
title={header === 'Id' ? 'Process Instance Id' : null}
>
{header}
</TableHeader>
))}
</TableRow>
</TableHead>
<tbody>{rows}</tbody>
</Table>
);
};
const toggleShowFilterOptions = () => {
setShowFilterOptions(!showFilterOptions);
};
const filterComponent = () => {
if (!filtersEnabled) {
return null;
}
return (
<>
<Grid fullWidth>
<Column
sm={{ span: 1, offset: 3 }}
md={{ span: 1, offset: 7 }}
lg={{ span: 1, offset: 15 }}
>
<Button
data-qa="filter-section-expand-toggle"
kind="ghost"
renderIcon={Filter}
iconDescription="Filter Options"
hasIconOnly
size="lg"
onClick={toggleShowFilterOptions}
/>
</Column>
</Grid>
{filterOptions()}
</>
);
};
if (pagination) {
// eslint-disable-next-line prefer-const
let { page, perPage } = getPageInfoFromSearchParams(
searchParams,
undefined,
undefined,
paginationQueryParamPrefix
);
if (perPageOptions && !perPageOptions.includes(perPage)) {
// eslint-disable-next-line prefer-destructuring
perPage = perPageOptions[1];
}
return (
<>
{filterComponent()}
<PaginationForTable
page={page}
perPage={perPage}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix}
perPageOptions={perPageOptions}
/>
</>
);
}
return null;
}

View File

@ -0,0 +1,64 @@
import { useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Button,
// @ts-ignore
} from '@carbon/react';
import { ProcessModel } from '../interfaces';
import HttpService from '../services/HttpService';
import ErrorContext from '../contexts/ErrorContext';
import { modifyProcessModelPath } from '../helpers';
type OwnProps = {
processModel: ProcessModel;
onSuccessCallback: Function;
className?: string;
};
export default function ProcessInstanceRun({
processModel,
onSuccessCallback,
className,
}: OwnProps) {
const navigate = useNavigate();
const setErrorMessage = (useContext as any)(ErrorContext)[1];
const modifiedProcessModelId = modifyProcessModelPath(processModel.id);
const onProcessInstanceRun = (processInstance: any) => {
// FIXME: ensure that the task is actually for the current user as well
const processInstanceId = (processInstance as any).id;
const nextTask = (processInstance as any).next_task;
if (nextTask && nextTask.state === 'READY') {
navigate(`/tasks/${processInstanceId}/${nextTask.id}`);
}
onSuccessCallback(processInstance);
};
const processModelRun = (processInstance: any) => {
setErrorMessage(null);
HttpService.makeCallToBackend({
path: `/process-instances/${processInstance.id}/run`,
successCallback: onProcessInstanceRun,
failureCallback: setErrorMessage,
httpMethod: 'POST',
});
};
const processInstanceCreateAndRun = () => {
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}/process-instances`,
successCallback: processModelRun,
httpMethod: 'POST',
});
};
return (
<Button
onClick={processInstanceCreateAndRun}
variant="primary"
className={className}
>
Run
</Button>
);
}

View File

@ -2,14 +2,9 @@ import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
// @ts-ignore
import { Button, ButtonSet, Form, Stack, TextInput } from '@carbon/react';
import {
getGroupFromModifiedModelId,
modifyProcessModelPath,
slugifyString,
} from '../helpers';
import { modifyProcessModelPath, slugifyString } from '../helpers';
import HttpService from '../services/HttpService';
import { ProcessModel } from '../interfaces';
import ButtonWithConfirmation from './ButtonWithConfirmation';
type OwnProps = {
mode: string;
@ -29,7 +24,6 @@ export default function ProcessModelForm({
useState<boolean>(false);
const [displayNameInvalid, setDisplayNameInvalid] = useState<boolean>(false);
const navigate = useNavigate();
const modifiedProcessModelPath = modifyProcessModelPath(processModel.id);
const navigateToProcessModel = (result: ProcessModel) => {
if ('id' in result) {
@ -40,30 +34,14 @@ export default function ProcessModelForm({
}
};
const navigateToProcessModels = (_result: any) => {
navigate(
`/admin/process-groups/${getGroupFromModifiedModelId(
modifiedProcessModelPath
)}`
);
};
const hasValidIdentifier = (identifierToCheck: string) => {
return identifierToCheck.match(/^[a-z0-9][0-9a-z-]+[a-z0-9]$/);
};
const deleteProcessModel = () => {
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelPath}`,
successCallback: navigateToProcessModels,
httpMethod: 'DELETE',
});
};
const handleFormSubmission = (event: any) => {
event.preventDefault();
let hasErrors = false;
if (!hasValidIdentifier(processModel.id)) {
if (mode === 'new' && !hasValidIdentifier(processModel.id)) {
setIdentifierInvalid(true);
hasErrors = true;
}
@ -74,10 +52,7 @@ export default function ProcessModelForm({
if (hasErrors) {
return;
}
let path = `/process-models`;
if (mode === 'edit') {
path = `/process-models/${modifiedProcessModelPath}`;
}
const path = `/process-models/${processGroupId}`;
let httpMethod = 'POST';
if (mode === 'edit') {
httpMethod = 'PUT';
@ -88,7 +63,7 @@ export default function ProcessModelForm({
};
if (mode === 'new') {
Object.assign(postBody, {
id: `${processGroupId}:${processModel.id}`,
id: `${processGroupId}/${processModel.id}`,
});
}
@ -175,16 +150,6 @@ export default function ProcessModelForm({
Submit
</Button>,
];
if (mode === 'edit') {
buttons.push(
<ButtonWithConfirmation
description={`Delete Process Model ${processModel.id}?`}
onConfirmation={deleteProcessModel}
buttonLabel="Delete"
confirmButtonLabel="Delete"
/>
);
}
return <ButtonSet>{buttons}</ButtonSet>;
};
return (

View File

@ -0,0 +1,105 @@
import { ReactElement, useEffect, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import {
Tile,
// @ts-ignore
} from '@carbon/react';
import HttpService from '../services/HttpService';
import { ProcessModel, ProcessInstance } from '../interfaces';
import { modifyProcessModelPath, truncateString } from '../helpers';
import ProcessInstanceRun from './ProcessInstanceRun';
type OwnProps = {
headerElement?: ReactElement;
};
export default function ProcessModelListTiles({ headerElement }: OwnProps) {
const [searchParams] = useSearchParams();
const [processModels, setProcessModels] = useState<ProcessModel[] | null>(
null
);
const [processInstance, setProcessInstance] =
useState<ProcessInstance | null>(null);
useEffect(() => {
const setProcessModelsFromResult = (result: any) => {
setProcessModels(result.results);
};
// only allow 10 for now until we get the backend only returnin certain models for user execution
const queryParams = '?per_page=10';
HttpService.makeCallToBackend({
path: `/process-models${queryParams}`,
successCallback: setProcessModelsFromResult,
});
}, [searchParams]);
const processInstanceRunResultTag = () => {
if (processInstance) {
return (
<div className="alert alert-success" role="alert">
<p>
Process Instance {processInstance.id} kicked off (
<Link
to={`/admin/process-models/${modifyProcessModelPath(
processInstance.process_model_identifier
)}/process-instances/${processInstance.id}`}
data-qa="process-instance-show-link"
>
view
</Link>
).
</p>
</div>
);
}
return null;
};
const processModelsDisplayArea = () => {
let displayText = null;
if (processModels && processModels.length > 0) {
displayText = (processModels || []).map((row: ProcessModel) => {
return (
<Tile
id="tile-1"
className="tile-process-group"
href={`/admin/process-models/${modifyProcessModelPath(row.id)}`}
>
<div className="tile-process-group-content-container">
<div className="tile-title-top">{row.display_name}</div>
<p className="tile-description">
{truncateString(row.description || '', 25)}
</p>
<ProcessInstanceRun
processModel={row}
onSuccessCallback={setProcessInstance}
className="tile-pin-bottom"
/>
</div>
</Tile>
);
});
} else {
displayText = <p>No Models To Display</p>;
}
return displayText;
};
const processModelArea = () => {
if (processModels && processModels.length > 0) {
return (
<>
{headerElement}
{processInstanceRunResultTag()}
{processModelsDisplayArea()}
</>
);
}
return null;
};
if (processModels) {
return <>{processModelArea()}</>;
}
return null;
}

View File

@ -0,0 +1,56 @@
import {
ComboBox,
// @ts-ignore
} from '@carbon/react';
import { truncateString } from '../helpers';
import { ProcessReference } from '../interfaces';
type OwnProps = {
onChange: (..._args: any[]) => any;
processes: ProcessReference[];
selectedItem?: ProcessReference | null;
titleText?: string;
height?: string;
};
export default function ProcessSearch({
processes,
selectedItem,
onChange,
titleText = 'Process Search',
height = '50px',
}: OwnProps) {
const shouldFilter = (options: any) => {
const process: ProcessReference = options.item;
const { inputValue } = options;
return (
inputValue === null ||
`${process.display_name} (${process.identifier})`
.toLowerCase()
.includes(inputValue.toLowerCase())
);
};
return (
<div style={{ width: '100%', height }}>
<ComboBox
onChange={onChange}
id="process-model-select"
data-qa="process-model-selection"
items={processes}
itemToString={(process: ProcessReference) => {
if (process) {
return `${process.display_name} (${truncateString(
process.identifier,
20
)})`;
}
return null;
}}
shouldFilterItem={shouldFilter}
placeholder="Choose a process"
titleText={titleText}
selectedItem={selectedItem}
/>
</div>
);
}

View File

@ -52,10 +52,14 @@ import TouchModule from 'diagram-js/lib/navigation/touch';
// @ts-expect-error TS(7016) FIXME
import ZoomScrollModule from 'diagram-js/lib/navigation/zoomscroll';
import { Can } from '@casl/react';
import HttpService from '../services/HttpService';
import ButtonWithConfirmation from './ButtonWithConfirmation';
import { makeid } from '../helpers';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
type OwnProps = {
processModelId: string;
@ -76,6 +80,7 @@ type OwnProps = {
onServiceTasksRequested?: (..._args: any[]) => any;
onJsonFilesRequested?: (..._args: any[]) => any;
onDmnFilesRequested?: (..._args: any[]) => any;
onSearchProcessModels?: (..._args: any[]) => any;
url?: string;
};
@ -99,6 +104,7 @@ export default function ReactDiagramEditor({
onServiceTasksRequested,
onJsonFilesRequested,
onDmnFilesRequested,
onSearchProcessModels,
url,
}: OwnProps) {
const [diagramXMLString, setDiagramXMLString] = useState('');
@ -107,6 +113,13 @@ export default function ReactDiagramEditor({
const alreadyImportedXmlRef = useRef(false);
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processModelShowPath]: ['PUT'],
[targetUris.processModelFileShowPath]: ['POST', 'GET', 'PUT', 'DELETE'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
useEffect(() => {
if (diagramModelerState) {
return;
@ -292,6 +305,12 @@ export default function ReactDiagramEditor({
diagramModeler.on('spiff.json_files.requested', (event: any) => {
handleServiceTasksRequested(event);
});
diagramModeler.on('spiff.callactivity.search', (event: any) => {
if (onSearchProcessModels) {
onSearchProcessModels(event.value, event.eventBus, event.element);
}
});
}, [
diagramModelerState,
diagramType,
@ -304,6 +323,7 @@ export default function ReactDiagramEditor({
onServiceTasksRequested,
onJsonFilesRequested,
onDmnFilesRequested,
onSearchProcessModels,
]);
useEffect(() => {
@ -517,20 +537,40 @@ export default function ReactDiagramEditor({
if (diagramType !== 'readonly') {
return (
<>
<Button onClick={handleSave} variant="danger">
Save
</Button>
{fileName && (
<ButtonWithConfirmation
description={`Delete file ${fileName}?`}
onConfirmation={handleDelete}
buttonLabel="Delete"
/>
)}
{onSetPrimaryFile && (
<Button onClick={handleSetPrimaryFile}>Set as primary file</Button>
)}
<Button onClick={downloadXmlFile}>Download xml</Button>
<Can
I="PUT"
a={targetUris.processModelFileShowPath}
ability={ability}
>
<Button onClick={handleSave}>Save</Button>
</Can>
<Can
I="DELETE"
a={targetUris.processModelFileShowPath}
ability={ability}
>
{fileName && (
<ButtonWithConfirmation
description={`Delete file ${fileName}?`}
onConfirmation={handleDelete}
buttonLabel="Delete"
/>
)}
</Can>
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
{onSetPrimaryFile && (
<Button onClick={handleSetPrimaryFile}>
Set as primary file
</Button>
)}
</Can>
<Can
I="GET"
a={targetUris.processModelFileShowPath}
ability={ability}
>
<Button onClick={downloadXmlFile}>Download xml</Button>
</Can>
</>
);
}

View File

@ -1,59 +0,0 @@
import { useEffect, useState } from 'react';
import Nav from 'react-bootstrap/Nav';
import { useLocation } from 'react-router-dom';
export default function SubNavigation() {
const location = useLocation();
const [activeKey, setActiveKey] = useState('');
useEffect(() => {
let newActiveKey = '/admin/process-groups';
if (location.pathname.match(/^\/admin\/messages\b/)) {
newActiveKey = '/admin/messages';
} else if (
location.pathname.match(/^\/admin\/process-instances\/reports\b/)
) {
newActiveKey = '/admin/process-instances/reports';
} else if (location.pathname.match(/^\/admin\/process-instances\b/)) {
newActiveKey = '/admin/process-instances';
} else if (location.pathname.match(/^\/admin\/secrets\b/)) {
newActiveKey = '/admin/secrets';
} else if (location.pathname.match(/^\/admin\/authentications\b/)) {
newActiveKey = '/admin/authentications';
} else if (location.pathname === '/') {
newActiveKey = '/';
} else if (location.pathname.match(/^\/tasks\b/)) {
newActiveKey = '/';
}
setActiveKey(newActiveKey);
}, [location]);
if (activeKey) {
return (
<Nav variant="tabs" activeKey={activeKey}>
<Nav.Item data-qa="nav-home">
<Nav.Link href="/">Home</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link href="/admin/process-groups">Process Models</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link href="/admin/process-instances">Process Instances</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link href="/admin/messages">Messages</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link href="/admin/secrets">Secrets</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link href="/admin/authentications">Authentications</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link href="/admin/process-instances/reports">Reports</Nav.Link>
</Nav.Item>
</Nav>
);
}
return null;
}

View File

@ -0,0 +1,142 @@
import { useEffect, useState } from 'react';
// @ts-ignore
import { Button, Table } from '@carbon/react';
import { Link, useSearchParams } from 'react-router-dom';
import PaginationForTable from './PaginationForTable';
import {
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessModelPath,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const paginationQueryParamPrefix = 'tasks_for_my_open_processes';
export default function MyOpenProcesses() {
const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null);
useEffect(() => {
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
const setTasksFromResult = (result: any) => {
setTasks(result.results);
setPagination(result.pagination);
};
HttpService.makeCallToBackend({
path: `/tasks/for-my-open-processes?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
}, [searchParams]);
const buildTable = () => {
const rows = tasks.map((row) => {
const rowToUse = row as any;
const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`;
const modifiedProcessModelIdentifier = modifyProcessModelPath(
rowToUse.process_model_identifier
);
return (
<tr key={rowToUse.id}>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
>
{rowToUse.process_model_display_name}
</Link>
</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
>
View {rowToUse.process_instance_id}
</Link>
</td>
<td
title={`task id: ${rowToUse.name}, spiffworkflow task guid: ${rowToUse.id}`}
>
{rowToUse.task_title}
</td>
<td>{rowToUse.process_instance_status}</td>
<td>{rowToUse.group_identifier || '-'}</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.created_at_in_seconds
) || '-'}
</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.updated_at_in_seconds
) || '-'}
</td>
<td>
<Button
variant="primary"
href={taskUrl}
hidden={rowToUse.process_instance_status === 'suspended'}
disabled={!rowToUse.current_user_is_potential_owner}
>
Go
</Button>
</td>
</tr>
);
});
return (
<Table striped bordered>
<thead>
<tr>
<th>Process Model</th>
<th>Process Instance</th>
<th>Task Name</th>
<th>Process Instance Status</th>
<th>Assigned Group</th>
<th>Process Started</th>
<th>Process Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
);
};
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return null;
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
return (
<>
<h1>Tasks for my open processes</h1>
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix}
/>
</>
);
};
if (pagination) {
return tasksComponent();
}
return null;
}

View File

@ -0,0 +1,143 @@
import { useEffect, useState } from 'react';
// @ts-ignore
import { Button, Table } from '@carbon/react';
import { Link, useSearchParams } from 'react-router-dom';
import PaginationForTable from './PaginationForTable';
import {
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessModelPath,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
export default function TasksWaitingForMe() {
const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null);
useEffect(() => {
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
'tasks_waiting_for_me'
);
const setTasksFromResult = (result: any) => {
setTasks(result.results);
setPagination(result.pagination);
};
HttpService.makeCallToBackend({
path: `/tasks/for-me?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
}, [searchParams]);
const buildTable = () => {
const rows = tasks.map((row) => {
const rowToUse = row as any;
const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`;
const modifiedProcessModelIdentifier = modifyProcessModelPath(
rowToUse.process_model_identifier
);
return (
<tr key={rowToUse.id}>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
>
{rowToUse.process_model_display_name}
</Link>
</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
>
View {rowToUse.process_instance_id}
</Link>
</td>
<td
title={`task id: ${rowToUse.name}, spiffworkflow task guid: ${rowToUse.id}`}
>
{rowToUse.task_title}
</td>
<td>{rowToUse.username}</td>
<td>{rowToUse.process_instance_status}</td>
<td>{rowToUse.group_identifier || '-'}</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.created_at_in_seconds
) || '-'}
</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.updated_at_in_seconds
) || '-'}
</td>
<td>
<Button
variant="primary"
href={taskUrl}
hidden={rowToUse.process_instance_status === 'suspended'}
disabled={!rowToUse.current_user_is_potential_owner}
>
Go
</Button>
</td>
</tr>
);
});
return (
<Table striped bordered>
<thead>
<tr>
<th>Process Model</th>
<th>Process Instance</th>
<th>Task Name</th>
<th>Process Started By</th>
<th>Process Instance Status</th>
<th>Assigned Group</th>
<th>Process Started</th>
<th>Process Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
);
};
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return null;
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
'tasks_waiting_for_me'
);
return (
<>
<h1>Tasks waiting for me</h1>
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix="tasks_waiting_for_me"
/>
</>
);
};
if (pagination) {
return tasksComponent();
}
return null;
}

View File

@ -0,0 +1,144 @@
import { useEffect, useState } from 'react';
// @ts-ignore
import { Button, Table } from '@carbon/react';
import { Link, useSearchParams } from 'react-router-dom';
import PaginationForTable from './PaginationForTable';
import {
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessModelPath,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const paginationQueryParamPrefix = 'tasks_waiting_for_my_groups';
export default function TasksForWaitingForMyGroups() {
const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null);
useEffect(() => {
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
const setTasksFromResult = (result: any) => {
setTasks(result.results);
setPagination(result.pagination);
};
HttpService.makeCallToBackend({
path: `/tasks/for-my-groups?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
}, [searchParams]);
const buildTable = () => {
const rows = tasks.map((row) => {
const rowToUse = row as any;
const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.task_id}`;
const modifiedProcessModelIdentifier = modifyProcessModelPath(
rowToUse.process_model_identifier
);
return (
<tr key={rowToUse.id}>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
>
{rowToUse.process_model_display_name}
</Link>
</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
>
View {rowToUse.process_instance_id}
</Link>
</td>
<td
title={`task id: ${rowToUse.name}, spiffworkflow task guid: ${rowToUse.id}`}
>
{rowToUse.task_title}
</td>
<td>{rowToUse.username}</td>
<td>{rowToUse.process_instance_status}</td>
<td>{rowToUse.group_identifier || '-'}</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.created_at_in_seconds
) || '-'}
</td>
<td>
{convertSecondsToFormattedDateTime(
rowToUse.updated_at_in_seconds
) || '-'}
</td>
<td>
<Button
variant="primary"
href={taskUrl}
hidden={rowToUse.process_instance_status === 'suspended'}
disabled={!rowToUse.current_user_is_potential_owner}
>
Go
</Button>
</td>
</tr>
);
});
return (
<Table striped bordered>
<thead>
<tr>
<th>Process Model</th>
<th>Process Instance</th>
<th>Task Name</th>
<th>Process Started By</th>
<th>Process Instance Status</th>
<th>Assigned Group</th>
<th>Process Started</th>
<th>Process Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
);
};
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return null;
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE,
undefined,
paginationQueryParamPrefix
);
return (
<>
<h1>Tasks waiting for my groups</h1>
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix}
/>
</>
);
};
if (pagination) {
return tasksComponent();
}
return null;
}

View File

@ -18,5 +18,6 @@ export const PROCESS_STATUSES = [
// with time: yyyy-MM-dd HH:mm:ss
export const DATE_TIME_FORMAT = 'yyyy-MM-dd HH:mm:ss';
export const TIME_FORMAT_HOURS_MINUTES = 'HH:mm';
export const DATE_FORMAT = 'yyyy-MM-dd';
export const DATE_FORMAT_CARBON = 'Y-m-d';

6
src/contexts/Can.tsx Normal file
View File

@ -0,0 +1,6 @@
import { createContext } from 'react';
import { Ability } from '@casl/ability';
import { createContextualCan } from '@casl/react';
export const AbilityContext = createContext(new Ability());
export const Can = createContextualCan(AbilityContext.Consumer);

View File

@ -1,4 +1,4 @@
import { convertSecondsToFormattedDate, slugifyString } from './helpers';
import { convertSecondsToFormattedDateString, slugifyString } from './helpers';
test('it can slugify a string', () => {
expect(slugifyString('hello---world_ and then Some such-')).toEqual(
@ -7,6 +7,6 @@ test('it can slugify a string', () => {
});
test('it can keep the correct date when converting seconds to date', () => {
const dateString = convertSecondsToFormattedDate(1666325400);
const dateString = convertSecondsToFormattedDateString(1666325400);
expect(dateString).toEqual('2022-10-21');
});

View File

@ -1,5 +1,9 @@
import { format } from 'date-fns';
import { DATE_TIME_FORMAT, DATE_FORMAT } from './config';
import {
DATE_TIME_FORMAT,
DATE_FORMAT,
TIME_FORMAT_HOURS_MINUTES,
} from './config';
import {
DEFAULT_PER_PAGE,
DEFAULT_PAGE,
@ -42,27 +46,72 @@ export const convertDateToSeconds = (
return null;
};
export const convertDateObjectToFormattedString = (dateObject: Date) => {
if (dateObject) {
return format(dateObject, DATE_FORMAT);
}
return null;
};
export const convertDateAndTimeStringsToDate = (
dateString: string,
timeString: string
) => {
if (dateString && timeString) {
return new Date(`${dateString}T${timeString}`);
}
return null;
};
export const convertDateAndTimeStringsToSeconds = (
dateString: string,
timeString: string
) => {
const dateObject = convertDateAndTimeStringsToDate(dateString, timeString);
if (dateObject) {
return convertDateToSeconds(dateObject);
}
return null;
};
export const convertStringToDate = (dateString: string) => {
if (dateString) {
// add midnight time to the date so it c uses the correct date
// after converting to timezone
return new Date(`${dateString}T00:10:00`);
return convertDateAndTimeStringsToSeconds(dateString, '00:10:00');
};
export const convertSecondsToDateObject = (seconds: number) => {
if (seconds) {
return new Date(seconds * 1000);
}
return null;
};
export const convertSecondsToFormattedDateTime = (seconds: number) => {
if (seconds) {
const dateObject = new Date(seconds * 1000);
const dateObject = convertSecondsToDateObject(seconds);
if (dateObject) {
return format(dateObject, DATE_TIME_FORMAT);
}
return null;
};
export const convertSecondsToFormattedDate = (seconds: number) => {
if (seconds) {
const dateObject = new Date(seconds * 1000);
return format(dateObject, DATE_FORMAT);
export const convertDateObjectToFormattedHoursMinutes = (dateObject: Date) => {
if (dateObject) {
return format(dateObject, TIME_FORMAT_HOURS_MINUTES);
}
return null;
};
export const convertSecondsToFormattedTimeHoursMinutes = (seconds: number) => {
const dateObject = convertSecondsToDateObject(seconds);
if (dateObject) {
return convertDateObjectToFormattedHoursMinutes(dateObject);
}
return null;
};
export const convertSecondsToFormattedDateString = (seconds: number) => {
const dateObject = convertSecondsToDateObject(seconds);
if (dateObject) {
return convertDateObjectToFormattedString(dateObject);
}
return null;
};
@ -79,11 +128,20 @@ export const objectIsEmpty = (obj: object) => {
export const getPageInfoFromSearchParams = (
searchParams: any,
defaultPerPage: string | number = DEFAULT_PER_PAGE,
defaultPage: string | number = DEFAULT_PAGE
defaultPage: string | number = DEFAULT_PAGE,
paginationQueryParamPrefix: string | null = null
) => {
const page = parseInt(searchParams.get('page') || defaultPage.toString(), 10);
const paginationQueryParamPrefixToUse = paginationQueryParamPrefix
? `${paginationQueryParamPrefix}_`
: '';
const page = parseInt(
searchParams.get(`${paginationQueryParamPrefixToUse}page`) ||
defaultPage.toString(),
10
);
const perPage = parseInt(
searchParams.get('per_page') || defaultPerPage.toString(),
searchParams.get(`${paginationQueryParamPrefixToUse}per_page`) ||
defaultPerPage.toString(),
10
);
@ -139,3 +197,16 @@ export const getGroupFromModifiedModelId = (modifiedId: string) => {
export const splitProcessModelId = (processModelId: string) => {
return processModelId.split('/');
};
export const refreshAtInterval = (
interval: number,
timeout: number,
func: Function
) => {
const intervalRef = setInterval(() => func(), interval * 1000);
const timeoutRef = setTimeout(
() => clearInterval(intervalRef),
timeout * 1000
);
return [intervalRef, timeoutRef];
};

View File

@ -0,0 +1,48 @@
// We may need to update usage of Ability when we update.
// They say they are going to rename PureAbility to Ability and remove the old class.
import { AbilityBuilder, Ability } from '@casl/ability';
import { useContext, useEffect } from 'react';
import { AbilityContext } from '../contexts/Can';
import { PermissionCheckResponseBody, PermissionsToCheck } from '../interfaces';
import HttpService from '../services/HttpService';
export const usePermissionFetcher = (
permissionsToCheck: PermissionsToCheck
) => {
const ability = useContext(AbilityContext);
useEffect(() => {
const processPermissionResult = (result: PermissionCheckResponseBody) => {
const oldRules = ability.rules;
const { can, cannot, rules } = new AbilityBuilder(Ability);
Object.keys(result.results).forEach((url: string) => {
const permissionVerbResults = result.results[url];
Object.keys(permissionVerbResults).forEach((permissionVerb: string) => {
const hasPermission = permissionVerbResults[permissionVerb];
if (hasPermission) {
can(permissionVerb, url);
} else {
cannot(permissionVerb, url);
}
});
});
oldRules.forEach((oldRule: any) => {
if (oldRule.inverted) {
cannot(oldRule.action, oldRule.subject);
} else {
can(oldRule.action, oldRule.subject);
}
});
ability.update(rules);
};
HttpService.makeCallToBackend({
path: `/permissions-check`,
httpMethod: 'POST',
successCallback: processPermissionResult,
postBody: { requests_to_check: permissionsToCheck },
});
});
return { ability };
};

View File

@ -0,0 +1,20 @@
import { useParams } from 'react-router-dom';
export const useUriListForPermissions = () => {
const params = useParams();
const targetUris = {
authenticationListPath: `/v1.0/authentications`,
messageInstanceListPath: '/v1.0/messages',
processGroupListPath: '/v1.0/process-groups',
processGroupShowPath: `/v1.0/process-groups/${params.process_group_id}`,
processInstanceActionPath: `/v1.0/process-models/${params.process_model_id}/process-instances`,
processInstanceListPath: '/v1.0/process-instances',
processModelCreatePath: `/v1.0/process-models/${params.process_group_id}`,
processModelFileCreatePath: `/v1.0/process-models/${params.process_model_id}/files`,
processModelFileShowPath: `/v1.0/process-models/${params.process_model_id}/files/${params.file_name}`,
processModelShowPath: `/v1.0/process-models/${params.process_model_id}`,
secretListPath: `/v1.0/secrets`,
};
return { targetUris };
};

View File

@ -5,21 +5,21 @@
color: white;
}
h1{
height: 36px;
font-family: 'IBM Plex Sans';
font-style: normal;
h1 {
font-weight: 400;
font-size: 28px;
line-height: 36px;
color: #161616;
flex: none;
order: 0;
align-self: stretch;
flex-grow: 0;
margin-bottom: 1em
}
h2 {
font-weight: 400;
font-size: 20px;
line-height: 28px;
color: #161616;
}
.span-tag {
color: black;
}
@ -31,7 +31,7 @@ h1{
border: 1px solid #393939;
}
.cds--btn.button-white-background:hover {
background: #525252;
background: lightgrey;
}
.cds--breadcrumb-item a.cds--link:hover {
@ -71,7 +71,7 @@ h1{
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
monospace;
}
.app-logo {
@ -182,9 +182,64 @@ h1.with-icons {
}
/* Json Web Form CSS Fix - Bootstrap now requries that each li have a "list-inline-item." Also have a PR
in on this with the react-jsonschema-form repo. This is just a patch fix to allow date inputs to layout a little more cleanly */
in on this with the react-jsonschema-form repo. This is just a patch fix to allow date inputs to layout a little more cleanly */
.list-inline>li {
display: inline-block;
padding-right: 5px;
padding-left: 5px;
}
.cds--tile.tile-process-group {
padding: 0px;
margin: 16px;
width: 354px;
height: 264px;
background: #F4F4F4;
order: 1;
float: left;
}
.tile-process-group-content-container {
width: 354px;
height: 264px;
padding: 1em;
position: relative;
}
.tile-process-group-display-name {
margin-top: 2em;
margin-bottom: 1em;
font-size: 20px;
line-height: 28px;
color: #161616;
order: 0;
}
.tile-title-top {
margin-bottom: 2em;
font-size: 20px;
line-height: 28px;
color: #161616;
order: 0;
}
.tile-description {
font-size: 14px;
line-height: 20px;
letter-spacing: 0.16px;
color: #161616;
order: 1;
}
.tile-process-group-children-count {
font-size: 14px;
line-height: 20px;
letter-spacing: 0.16px;
color: #161616;
order: 1;
}
.tile-pin-bottom {
position: absolute;
bottom: 1em;
}

View File

@ -11,16 +11,18 @@ export interface RecentProcessModel {
processModelDisplayName: string;
}
export interface ProcessGroup {
id: string;
display_name: string;
description?: string | null;
}
export interface ProcessFileReference {
export interface ProcessReference {
id: string; // The unique id of the process or decision table.
name: string; // The process or decision table name.
name: string; // The process or decision Display name.
identifier: string;
display_name: string;
process_group_id: string;
process_model_id: string;
type: string; // either "decision" or "process"
file_name: string;
has_lanes: boolean;
is_executable: boolean;
is_primary: boolean;
}
export interface ProcessFile {
@ -28,12 +30,17 @@ export interface ProcessFile {
last_modified: string;
name: string;
process_model_id: string;
references: ProcessFileReference[];
references: ProcessReference[];
size: number;
type: string;
file_contents?: string;
}
export interface ProcessInstance {
id: number;
process_model_identifier: string;
}
export interface ProcessModel {
id: string;
description: string;
@ -42,6 +49,14 @@ export interface ProcessModel {
files: ProcessFile[];
}
export interface ProcessGroup {
id: string;
display_name: string;
description?: string | null;
process_models?: ProcessModel[];
process_groups?: ProcessGroup[];
}
// tuple of display value and URL
export type HotCrumbItem = [displayValue: string, url?: string];
@ -70,3 +85,28 @@ export interface PaginationObject {
export interface CarbonComboBoxSelection {
selectedItem: ProcessModel;
}
export interface CarbonComboBoxProcessSelection {
selectedItem: ProcessReference;
}
export interface PermissionsToCheck {
[key: string]: string[];
}
export interface PermissionVerbResults {
[key: string]: boolean;
}
export interface PermissionCheckResult {
[key: string]: PermissionVerbResults;
}
export interface PermissionCheckResponseBody {
results: PermissionCheckResult;
}
export interface FormField {
id: string;
title: string;
required: boolean;
type: string;
enum: string[];
}

View File

@ -20,10 +20,8 @@ import ReactFormEditor from './ReactFormEditor';
import ErrorContext from '../contexts/ErrorContext';
import ProcessInstanceLogList from './ProcessInstanceLogList';
import MessageInstanceList from './MessageInstanceList';
import SecretList from './SecretList';
import SecretNew from './SecretNew';
import SecretShow from './SecretShow';
import AuthenticationList from './AuthenticationList';
import Configuration from './Configuration';
import JsonSchemaFormBuilder from './JsonSchemaFormBuilder';
export default function AdminRoutes() {
const location = useLocation();
@ -110,10 +108,11 @@ export default function AdminRoutes() {
/>
<Route path="process-instances" element={<ProcessInstanceList />} />
<Route path="messages" element={<MessageInstanceList />} />
<Route path="secrets" element={<SecretList />} />
<Route path="secrets/new" element={<SecretNew />} />
<Route path="secrets/:key" element={<SecretShow />} />
<Route path="authentications" element={<AuthenticationList />} />
<Route path="configuration/*" element={<Configuration />} />
<Route
path="process-models/:process_model_id/form-builder"
element={<JsonSchemaFormBuilder />}
/>
</Routes>
);
}

View File

@ -0,0 +1,5 @@
import MyCompletedInstances from '../components/MyCompletedInstances';
export default function CompletedInstances() {
return <MyCompletedInstances />;
}

View File

@ -0,0 +1,65 @@
import { useContext, useEffect, useState } from 'react';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
// @ts-ignore
import { Tabs, TabList, Tab } from '@carbon/react';
import { Can } from '@casl/react';
import ErrorContext from '../contexts/ErrorContext';
import SecretList from './SecretList';
import SecretNew from './SecretNew';
import SecretShow from './SecretShow';
import AuthenticationList from './AuthenticationList';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
export default function Configuration() {
const location = useLocation();
const setErrorMessage = (useContext as any)(ErrorContext)[1];
const [selectedTabIndex, setSelectedTabIndex] = useState<number>(0);
const navigate = useNavigate();
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.authenticationListPath]: ['GET'],
[targetUris.secretListPath]: ['GET'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
useEffect(() => {
setErrorMessage(null);
let newSelectedTabIndex = 0;
if (location.pathname.match(/^\/admin\/configuration\/authentications\b/)) {
newSelectedTabIndex = 1;
}
setSelectedTabIndex(newSelectedTabIndex);
}, [location, setErrorMessage]);
return (
<>
<Tabs selectedIndex={selectedTabIndex}>
<TabList aria-label="List of tabs">
<Can I="GET" a={targetUris.secretListPath} ability={ability}>
<Tab onClick={() => navigate('/admin/configuration/secrets')}>
Secrets
</Tab>
</Can>
<Can I="GET" a={targetUris.authenticationListPath} ability={ability}>
<Tab
onClick={() => navigate('/admin/configuration/authentications')}
>
Authentications
</Tab>
</Can>
</TabList>
</Tabs>
<br />
<Routes>
<Route path="/" element={<SecretList />} />
<Route path="secrets" element={<SecretList />} />
<Route path="secrets/new" element={<SecretNew />} />
<Route path="secrets/:key" element={<SecretShow />} />
<Route path="authentications" element={<AuthenticationList />} />
</Routes>
</>
);
}

View File

@ -0,0 +1,9 @@
import ProcessModelListTiles from '../components/ProcessModelListTiles';
export default function CreateNewInstance() {
return (
<ProcessModelListTiles
headerElement={<h1>Process models available to you</h1>}
/>
);
}

View File

@ -0,0 +1,15 @@
import TasksForMyOpenProcesses from '../components/TasksForMyOpenProcesses';
import TasksWaitingForMe from '../components/TasksWaitingForMe';
import TasksForWaitingForMyGroups from '../components/TasksWaitingForMyGroups';
export default function GroupedTasks() {
return (
<>
<TasksForMyOpenProcesses />
<br />
<TasksWaitingForMe />
<br />
<TasksForWaitingForMyGroups />
</>
);
}

View File

@ -0,0 +1,67 @@
import { useContext, useEffect, useState } from 'react';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
// @ts-ignore
import { Tabs, TabList, Tab } from '@carbon/react';
import TaskShow from './TaskShow';
import ErrorContext from '../contexts/ErrorContext';
import MyTasks from './MyTasks';
import GroupedTasks from './GroupedTasks';
import CompletedInstances from './CompletedInstances';
import CreateNewInstance from './CreateNewInstance';
export default function HomePageRoutes() {
const location = useLocation();
const setErrorMessage = (useContext as any)(ErrorContext)[1];
const [selectedTabIndex, setSelectedTabIndex] = useState<number>(0);
const navigate = useNavigate();
useEffect(() => {
setErrorMessage(null);
let newSelectedTabIndex = 0;
if (location.pathname.match(/^\/tasks\/grouped\b/)) {
newSelectedTabIndex = 1;
} else if (location.pathname.match(/^\/tasks\/completed-instances\b/)) {
newSelectedTabIndex = 2;
} else if (location.pathname.match(/^\/tasks\/create-new-instance\b/)) {
newSelectedTabIndex = 3;
}
setSelectedTabIndex(newSelectedTabIndex);
}, [location, setErrorMessage]);
const renderTabs = () => {
if (location.pathname.match(/^\/tasks\/\d+\/\b/)) {
return null;
}
return (
<>
<Tabs selectedIndex={selectedTabIndex}>
<TabList aria-label="List of tabs">
<Tab onClick={() => navigate('/tasks/my-tasks')}>My Tasks</Tab>
<Tab onClick={() => navigate('/tasks/grouped')}>Grouped Tasks</Tab>
<Tab onClick={() => navigate('/tasks/completed-instances')}>
Completed Instances
</Tab>
<Tab onClick={() => navigate('/tasks/create-new-instance')}>
Create New Instance +
</Tab>
</TabList>
</Tabs>
<br />
</>
);
};
return (
<>
{renderTabs()}
<Routes>
<Route path="/" element={<MyTasks />} />
<Route path="my-tasks" element={<MyTasks />} />
<Route path=":process_instance_id/:task_id" element={<TaskShow />} />
<Route path="grouped" element={<GroupedTasks />} />
<Route path="completed-instances" element={<CompletedInstances />} />
<Route path="create-new-instance" element={<CreateNewInstance />} />
</Routes>
</>
);
}

View File

@ -0,0 +1,250 @@
import { useEffect, useState } from 'react';
// @ts-ignore
import { Button, Select, SelectItem, TextInput } from '@carbon/react';
import { useParams } from 'react-router-dom';
import { FormField } from '../interfaces';
import { modifyProcessModelPath, slugifyString } from '../helpers';
import HttpService from '../services/HttpService';
export default function JsonSchemaFormBuilder() {
const params = useParams();
const formFieldTypes = ['textbox', 'checkbox', 'select'];
const [formTitle, setFormTitle] = useState<string>('');
const [formDescription, setFormDescription] = useState<string>('');
const [formId, setFormId] = useState<string>('');
const [formFields, setFormFields] = useState<FormField[]>([]);
const [showNewFormField, setShowNewFormField] = useState<boolean>(false);
const [formFieldSelectOptions, setFormFieldSelectOptions] =
useState<string>('');
const [formIdHasBeenUpdatedByUser, setFormIdHasBeenUpdatedByUser] =
useState<boolean>(false);
const [formFieldIdHasBeenUpdatedByUser, setFormFieldIdHasBeenUpdatedByUser] =
useState<boolean>(false);
const [showFormFieldSelectTextField, setShowFormFieldSelectTextField] =
useState<boolean>(false);
const [formFieldId, setFormFieldId] = useState<string>('');
const [formFieldTitle, setFormFieldTitle] = useState<string>('');
const [formFieldType, setFormFieldType] = useState<string>('');
const modifiedProcessModelId = modifyProcessModelPath(
`${params.process_model_id}`
);
useEffect(() => {}, []);
const renderFormJson = () => {
const formJson = {
title: formTitle,
description: formDescription,
properties: {},
required: [],
};
formFields.forEach((formField: FormField) => {
let jsonSchemaFieldType = 'string';
if (formField.type === 'checkbox') {
jsonSchemaFieldType = 'boolean';
}
const formJsonObject: any = {
type: jsonSchemaFieldType,
title: formField.title,
};
if (formField.type === 'select') {
formJsonObject.enum = formField.enum;
}
(formJson.properties as any)[formField.id] = formJsonObject;
});
return JSON.stringify(formJson, null, 2);
};
const renderFormUiJson = () => {
const uiOrder = formFields.map((formField: FormField) => {
return formField.id;
});
return JSON.stringify({ 'ui:order': uiOrder }, null, 2);
};
const onFormFieldTitleChange = (newFormFieldTitle: string) => {
console.log('newFormFieldTitle', newFormFieldTitle);
console.log(
'setFormFieldIdHasBeenUpdatedByUser',
formFieldIdHasBeenUpdatedByUser
);
if (!formFieldIdHasBeenUpdatedByUser) {
setFormFieldId(slugifyString(newFormFieldTitle));
}
setFormFieldTitle(newFormFieldTitle);
};
const onFormTitleChange = (newFormTitle: string) => {
if (!formIdHasBeenUpdatedByUser) {
setFormId(slugifyString(newFormTitle));
}
setFormTitle(newFormTitle);
};
const addFormField = () => {
const newFormField: FormField = {
id: formFieldId,
title: formFieldTitle,
required: false,
type: formFieldType,
enum: formFieldSelectOptions.split(','),
};
setFormFieldIdHasBeenUpdatedByUser(false);
setShowNewFormField(false);
setFormFields([...formFields, newFormField]);
};
const handleFormFieldTypeChange = (event: any) => {
setFormFieldType(event.srcElement.value);
if (event.srcElement.value === 'select') {
setShowFormFieldSelectTextField(true);
} else {
setShowFormFieldSelectTextField(false);
}
};
const newFormFieldComponent = () => {
if (showNewFormField) {
return (
<>
<TextInput
id="form-field-title"
name="title"
labelText="Title"
value={formFieldTitle}
onChange={(event: any) => {
onFormFieldTitleChange(event.srcElement.value);
}}
/>
<TextInput
id="json-form-field-id"
name="id"
labelText="ID"
value={formFieldId}
onChange={(event: any) => {
setFormFieldIdHasBeenUpdatedByUser(true);
setFormFieldId(event.srcElement.value);
}}
/>
<Select
id="form-field-type"
labelText="Type"
onChange={handleFormFieldTypeChange}
>
{formFieldTypes.map((fft: string) => {
return <SelectItem text={fft} value={fft} />;
})}
</Select>
{showFormFieldSelectTextField ? (
<TextInput
id="json-form-field-select-options"
name="select-options"
labelText="Select Options"
onChange={(event: any) => {
setFormFieldSelectOptions(event.srcElement.value);
}}
/>
) : null}
<Button onClick={addFormField}>Add Field</Button>
</>
);
}
return null;
};
const formFieldArea = () => {
if (formFields.length > 0) {
return formFields.map((formField: FormField) => {
return <p>Form Field: {formField.id}</p>;
});
}
return null;
};
const handleSaveCallback = (result: any) => {
console.log('result', result);
};
const uploadFile = (file: File) => {
const url = `/process-models/${modifiedProcessModelId}/files`;
const httpMethod = 'POST';
const formData = new FormData();
formData.append('file', file);
formData.append('fileName', file.name);
HttpService.makeCallToBackend({
path: url,
successCallback: handleSaveCallback,
httpMethod,
postBody: formData,
});
};
const saveFile = () => {
const formJsonFileName = `${formId}-schema.json`;
const formUiJsonFileName = `${formId}-uischema.json`;
uploadFile(new File([renderFormJson()], formJsonFileName));
uploadFile(new File([renderFormUiJson()], formUiJsonFileName));
};
const jsonFormArea = () => {
return (
<>
<Button onClick={saveFile}>Save</Button>
<TextInput
id="json-form-title"
name="title"
labelText="Title"
value={formTitle}
onChange={(event: any) => {
onFormTitleChange(event.srcElement.value);
}}
/>
<TextInput
id="json-form-id"
name="id"
labelText="ID"
value={formId}
onChange={(event: any) => {
setFormIdHasBeenUpdatedByUser(true);
setFormId(event.srcElement.value);
}}
/>
<TextInput
id="form-description"
name="description"
labelText="Description"
value={formDescription}
onChange={(event: any) => {
setFormDescription(event.srcElement.value);
}}
/>
<Button
onClick={() => {
setFormFieldId('');
setFormFieldTitle('');
setFormFieldType('');
setFormFieldSelectOptions('');
setShowFormFieldSelectTextField(false);
setShowNewFormField(true);
}}
>
New Field
</Button>
{formFieldArea()}
{newFormFieldComponent()}
</>
);
};
return <>{jsonFormArea()}</>;
}

View File

@ -5,7 +5,7 @@ import { Link, useParams, useSearchParams } from 'react-router-dom';
import PaginationForTable from '../components/PaginationForTable';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import {
convertSecondsToFormattedDate,
convertSecondsToFormattedDateString,
getPageInfoFromSearchParams,
modifyProcessModelPath,
unModifyProcessModelPath,
@ -65,10 +65,12 @@ export default function MessageInstanceList() {
</td>
<td>{rowToUse.message_identifier}</td>
<td>{rowToUse.message_type}</td>
<td>{rowToUse.failure_cause}</td>
<td>{rowToUse.failure_cause || '-'}</td>
<td>{rowToUse.status}</td>
<td>
{convertSecondsToFormattedDate(rowToUse.created_at_in_seconds)}
{convertSecondsToFormattedDateString(
rowToUse.created_at_in_seconds
)}
</td>
</tr>
);
@ -94,14 +96,8 @@ export default function MessageInstanceList() {
if (pagination) {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
let queryParamString = '';
let breadcrumbElement = null;
if (searchParams.get('process_instance_id')) {
queryParamString += `&process_group_id=${searchParams.get(
'process_group_id'
)}&process_model_id=${searchParams.get(
'process_model_id'
)}&process_instance_id=${searchParams.get('process_instance_id')}`;
breadcrumbElement = (
<ProcessBreadcrumb
hotCrumbs={[
@ -132,8 +128,6 @@ export default function MessageInstanceList() {
perPage={perPage}
pagination={pagination}
tableToDisplay={buildTable()}
queryParamString={queryParamString}
path="/admin/messages"
/>
</>
);

View File

@ -6,30 +6,38 @@ import PaginationForTable from '../components/PaginationForTable';
import {
getPageInfoFromSearchParams,
modifyProcessModelPath,
refreshAtInterval,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject, RecentProcessModel } from '../interfaces';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const REFRESH_INTERVAL = 10;
const REFRESH_TIMEOUT = 600;
export default function HomePage() {
export default function MyTasks() {
const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null);
useEffect(() => {
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE
);
const setTasksFromResult = (result: any) => {
setTasks(result.results);
setPagination(result.pagination);
const getTasks = () => {
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
PER_PAGE_FOR_TASKS_ON_HOME_PAGE
);
const setTasksFromResult = (result: any) => {
setTasks(result.results);
setPagination(result.pagination);
};
HttpService.makeCallToBackend({
path: `/tasks?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
};
HttpService.makeCallToBackend({
path: `/tasks?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
getTasks();
refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks);
}, [searchParams]);
let recentProcessModels: RecentProcessModel[] = [];
@ -122,7 +130,7 @@ export default function HomePage() {
});
return (
<>
<h1>Processes I can start</h1>
<h1>Recently viewed process models</h1>
<Table striped bordered>
<thead>
<tr>
@ -152,7 +160,6 @@ export default function HomePage() {
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
path="/tasks"
/>
</>
);
@ -170,6 +177,7 @@ export default function HomePage() {
return (
<>
{tasksWaitingForMe}
<br />
{relevantProcessModelSection}
</>
);

View File

@ -1,42 +1,34 @@
import { useEffect, useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';
import {
Button,
Table,
// ExpandableTile,
// TileAboveTheFoldContent,
// TileBelowTheFoldContent,
// TextInput,
// ClickableTile,
// @ts-ignore
} from '@carbon/react';
import { Can } from '@casl/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import PaginationForTable from '../components/PaginationForTable';
import HttpService from '../services/HttpService';
import {
getPageInfoFromSearchParams,
modifyProcessModelPath,
} from '../helpers';
import { CarbonComboBoxSelection, ProcessGroup } from '../interfaces';
import { modifyProcessModelPath } from '../helpers';
import { CarbonComboBoxSelection, PermissionsToCheck } from '../interfaces';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { usePermissionFetcher } from '../hooks/PermissionService';
import ProcessModelSearch from '../components/ProcessModelSearch';
import ProcessGroupListTiles from '../components/ProcessGroupListTiles';
// Example process group json
// {'process_group_id': 'sure', 'display_name': 'Test Workflows', 'id': 'test_process_group'}
export default function ProcessGroupList() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [processGroups, setProcessGroups] = useState([]);
const [pagination, setPagination] = useState(null);
const [processModelAvailableItems, setProcessModelAvailableItems] = useState(
[]
);
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processGroupListPath]: ['POST'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
useEffect(() => {
const setProcessGroupsFromResult = (result: any) => {
setProcessGroups(result.results);
setPagination(result.pagination);
};
const processResultForProcessModels = (result: any) => {
const selectionArray = result.results.map((item: any) => {
const label = `${item.id}`;
@ -45,13 +37,6 @@ export default function ProcessGroupList() {
});
setProcessModelAvailableItems(selectionArray);
};
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
// for browsing
HttpService.makeCallToBackend({
path: `/process-groups?per_page=${perPage}&page=${page}`,
successCallback: setProcessGroupsFromResult,
});
// for search box
HttpService.makeCallToBackend({
path: `/process-models?per_page=1000`,
@ -59,66 +44,6 @@ export default function ProcessGroupList() {
});
}, [searchParams]);
const buildTable = () => {
const rows = processGroups.map((row: ProcessGroup) => {
return (
<tr key={(row as any).id}>
<td>
<Link
to={`/admin/process-groups/${(row as any).id}`}
title={(row as any).id}
>
{(row as any).display_name}
</Link>
</td>
</tr>
);
});
return (
<Table striped bordered>
<thead>
<tr>
<th>Process Group</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
);
// const rows = processGroups.map((row: ProcessGroup) => {
// return (
// <span>
// <ClickableTile href={`/admin/process-groups/${row.id}`}>
// {row.display_name}
// </ClickableTile>
// </span>
// );
// });
//
// return <div style={{ width: '400px' }}>{rows}</div>;
};
const processGroupsDisplayArea = () => {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
let displayText = null;
if (processGroups?.length > 0) {
displayText = (
<>
<h3>Browse</h3>
<PaginationForTable
page={page}
perPage={perPage}
pagination={pagination as any}
tableToDisplay={buildTable()}
path="/admin/process-groups"
/>
</>
);
} else {
displayText = <p>No Groups To Display</p>;
}
return displayText;
};
const processModelSearchArea = () => {
const processModelSearchOnChange = (selection: CarbonComboBoxSelection) => {
const processModel = selection.selectedItem;
@ -135,18 +60,21 @@ export default function ProcessGroupList() {
);
};
if (pagination) {
if (processModelAvailableItems) {
return (
<>
<ProcessBreadcrumb hotCrumbs={[['Process Groups']]} />
<Button kind="secondary" href="/admin/process-groups/new">
Add a process group
</Button>
<br />
<Can I="POST" a={targetUris.processGroupListPath} ability={ability}>
<Button kind="secondary" href="/admin/process-groups/new">
Add a process group
</Button>
<br />
<br />
</Can>
<br />
{processModelSearchArea()}
<br />
{processGroupsDisplayArea()}
<ProcessGroupListTiles />
</>
);
}

View File

@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { Link, useSearchParams, useParams } from 'react-router-dom';
// @ts-ignore
import { Button, Table, Stack } from '@carbon/react';
import { Can } from '@casl/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import PaginationForTable from '../components/PaginationForTable';
import HttpService from '../services/HttpService';
@ -10,7 +11,15 @@ import {
modifyProcessModelPath,
unModifyProcessModelPath,
} from '../helpers';
import { ProcessGroup, ProcessModel } from '../interfaces';
import {
PaginationObject,
PermissionsToCheck,
ProcessGroup,
ProcessModel,
} from '../interfaces';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { usePermissionFetcher } from '../hooks/PermissionService';
import ProcessGroupListTiles from '../components/ProcessGroupListTiles';
export default function ProcessGroupShow() {
const params = useParams();
@ -18,9 +27,16 @@ export default function ProcessGroupShow() {
const [processGroup, setProcessGroup] = useState<ProcessGroup | null>(null);
const [processModels, setProcessModels] = useState([]);
const [processGroups, setProcessGroups] = useState([]);
const [modelPagination, setModelPagination] = useState(null);
const [groupPagination, setGroupPagination] = useState(null);
const [modelPagination, setModelPagination] =
useState<PaginationObject | null>(null);
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processGroupListPath]: ['POST'],
[targetUris.processGroupShowPath]: ['PUT'],
[targetUris.processModelCreatePath]: ['POST'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
useEffect(() => {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
@ -29,10 +45,6 @@ export default function ProcessGroupShow() {
setProcessModels(result.results);
setModelPagination(result.pagination);
};
const setProcessGroupFromResult = (result: any) => {
setProcessGroups(result.results);
setGroupPagination(result.pagination);
};
const processResult = (result: any) => {
setProcessGroup(result);
const unmodifiedProcessGroupId = unModifyProcessModelPath(
@ -42,10 +54,6 @@ export default function ProcessGroupShow() {
path: `/process-models?process_group_identifier=${unmodifiedProcessGroupId}&per_page=${perPage}&page=${page}`,
successCallback: setProcessModelFromResult,
});
HttpService.makeCallToBackend({
path: `/process-groups?process_group_identifier=${unmodifiedProcessGroupId}&per_page=${perPage}&page=${page}`,
successCallback: setProcessGroupFromResult,
});
};
HttpService.makeCallToBackend({
path: `/process-groups/${params.process_group_id}`,
@ -58,7 +66,9 @@ export default function ProcessGroupShow() {
return null;
}
const rows = processModels.map((row: ProcessModel) => {
const modifiedProcessModelId: String = modifyProcessModelPath((row as any).id);
const modifiedProcessModelId: String = modifyProcessModelPath(
(row as any).id
);
return (
<tr key={row.id}>
<td>
@ -75,7 +85,7 @@ export default function ProcessGroupShow() {
});
return (
<div>
<h3>Process Models</h3>
<h2>Process Models</h2>
<Table striped bordered>
<thead>
<tr>
@ -89,43 +99,7 @@ export default function ProcessGroupShow() {
);
};
const buildGroupTable = () => {
if (processGroup === null) {
return null;
}
const rows = processGroups.map((row: ProcessGroup) => {
const modifiedProcessGroupId: String = modifyProcessModelPath(row.id);
return (
<tr key={row.id}>
<td>
<Link
to={`/admin/process-groups/${modifiedProcessGroupId}`}
data-qa="process-model-show-link"
>
{row.id}
</Link>
</td>
<td>{row.display_name}</td>
</tr>
);
});
return (
<div>
<h3>Process Groups</h3>
<Table striped bordered>
<thead>
<tr>
<th>Process Group Id</th>
<th>Display Name</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
</div>
);
};
if (processGroup && groupPagination && modelPagination) {
if (processGroup && modelPagination) {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
const modifiedProcessGroupId = modifyProcessModelPath(processGroup.id);
return (
@ -136,43 +110,51 @@ export default function ProcessGroupShow() {
['', `process_group:${processGroup.id}`],
]}
/>
<h1>Process Group: {processGroup.display_name}</h1>
<ul>
<Stack orientation="horizontal" gap={3}>
<Button
kind="secondary"
href={`/admin/process-groups/new?parentGroupId=${processGroup.id}`}
<Can I="POST" a={targetUris.processGroupListPath} ability={ability}>
<Button
href={`/admin/process-groups/new?parentGroupId=${processGroup.id}`}
>
Add a process group
</Button>
</Can>
<Can
I="POST"
a={targetUris.processModelCreatePath}
ability={ability}
>
Add a process group
</Button>
<Button
href={`/admin/process-models/${modifiedProcessGroupId}/new`}
>
Add a process model
</Button>
<Button
href={`/admin/process-groups/${modifiedProcessGroupId}/edit`}
variant="secondary"
>
Edit process group
</Button>
<Button
href={`/admin/process-models/${modifiedProcessGroupId}/new`}
>
Add a process model
</Button>
</Can>
<Can I="PUT" a={targetUris.processGroupShowPath} ability={ability}>
<Button
href={`/admin/process-groups/${modifiedProcessGroupId}/edit`}
>
Edit process group
</Button>
</Can>
</Stack>
<br />
<br />
<PaginationForTable
page={page}
perPage={perPage}
pagination={modelPagination}
tableToDisplay={buildModelTable()}
path={`/admin/process-groups/${processGroup.id}`}
/>
{/* eslint-disable-next-line sonarjs/no-gratuitous-expressions */}
{modelPagination && modelPagination.total > 0 && (
<PaginationForTable
page={page}
perPage={perPage}
pagination={modelPagination}
tableToDisplay={buildModelTable()}
/>
)}
<br />
<br />
<PaginationForTable
page={page}
perPage={perPage}
pagination={groupPagination}
tableToDisplay={buildGroupTable()}
path={`/admin/process-groups/${processGroup.id}`}
<ProcessGroupListTiles
processGroup={processGroup}
headerElement={<h2>Process Groups</h2>}
/>
</ul>
</>

View File

@ -1,477 +1,15 @@
import { useContext, useEffect, useMemo, useState } from 'react';
import {
Link,
useNavigate,
useParams,
useSearchParams,
} from 'react-router-dom';
import { useSearchParams } from 'react-router-dom';
// @ts-ignore
import { Filter } from '@carbon/icons-react';
import {
Button,
ButtonSet,
DatePicker,
DatePickerInput,
Table,
Grid,
Column,
MultiSelect,
TableHeader,
TableHead,
TableRow,
// @ts-ignore
} from '@carbon/react';
import { PROCESS_STATUSES, DATE_FORMAT, DATE_FORMAT_CARBON } from '../config';
import {
convertDateStringToSeconds,
convertSecondsToFormattedDate,
getPageInfoFromSearchParams,
getProcessModelFullIdentifierFromSearchParams,
modifyProcessModelPath,
} from '../helpers';
import PaginationForTable from '../components/PaginationForTable';
import 'react-datepicker/dist/react-datepicker.css';
import ErrorContext from '../contexts/ErrorContext';
import HttpService from '../services/HttpService';
import 'react-bootstrap-typeahead/css/Typeahead.css';
import 'react-bootstrap-typeahead/css/Typeahead.bs5.css';
import { PaginationObject, ProcessModel } from '../interfaces';
import ProcessModelSearch from '../components/ProcessModelSearch';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
import { getProcessModelFullIdentifierFromSearchParams } from '../helpers';
export default function ProcessInstanceList() {
const params = useParams();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [processInstances, setProcessInstances] = useState([]);
const [reportMetadata, setReportMetadata] = useState({});
const [pagination, setPagination] = useState<PaginationObject | null>(null);
const oneHourInSeconds = 3600;
const oneMonthInSeconds = oneHourInSeconds * 24 * 30;
const [startFrom, setStartFrom] = useState<string>('');
const [startTo, setStartTo] = useState<string>('');
const [endFrom, setEndFrom] = useState<string>('');
const [endTo, setEndTo] = useState<string>('');
const [showFilterOptions, setShowFilterOptions] = useState<boolean>(false);
const setErrorMessage = (useContext as any)(ErrorContext)[1];
const [processStatusAllOptions, setProcessStatusAllOptions] = useState<any[]>(
[]
);
const [processStatusSelection, setProcessStatusSelection] = useState<
string[]
>([]);
const [processModelAvailableItems, setProcessModelAvailableItems] = useState<
ProcessModel[]
>([]);
const [processModelSelection, setProcessModelSelection] =
useState<ProcessModel | null>(null);
const parametersToAlwaysFilterBy = useMemo(() => {
return {
start_from: setStartFrom,
start_to: setStartTo,
end_from: setEndFrom,
end_to: setEndTo,
};
}, [setStartFrom, setStartTo, setEndFrom, setEndTo]);
const parametersToGetFromSearchParams = useMemo(() => {
return {
process_model_identifier: null,
process_status: null,
};
}, []);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
function setProcessInstancesFromResult(result: any) {
const processInstancesFromApi = result.results;
setProcessInstances(processInstancesFromApi);
setReportMetadata(result.report_metadata);
setPagination(result.pagination);
}
function getProcessInstances() {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
let queryParamString = `per_page=${perPage}&page=${page}`;
Object.keys(parametersToAlwaysFilterBy).forEach((paramName: string) => {
// @ts-expect-error TS(7053) FIXME:
const functionToCall = parametersToAlwaysFilterBy[paramName];
const searchParamValue = searchParams.get(paramName);
if (searchParamValue) {
queryParamString += `&${paramName}=${searchParamValue}`;
const dateString = convertSecondsToFormattedDate(
searchParamValue as any
);
functionToCall(dateString);
setShowFilterOptions(true);
}
});
Object.keys(parametersToGetFromSearchParams).forEach(
(paramName: string) => {
if (searchParams.get(paramName)) {
// @ts-expect-error TS(7053) FIXME:
const functionToCall = parametersToGetFromSearchParams[paramName];
queryParamString += `&${paramName}=${searchParams.get(paramName)}`;
if (functionToCall !== null) {
functionToCall(searchParams.get(paramName) || '');
}
setShowFilterOptions(true);
}
}
);
HttpService.makeCallToBackend({
path: `/process-instances?${queryParamString}`,
successCallback: setProcessInstancesFromResult,
});
}
function processResultForProcessModels(result: any) {
const processModelFullIdentifier =
getProcessModelFullIdentifierFromSearchParams(searchParams);
const selectionArray = result.results.map((item: any) => {
const label = `${item.id}`;
Object.assign(item, { label });
if (label === processModelFullIdentifier) {
setProcessModelSelection(item);
}
return item;
});
setProcessModelAvailableItems(selectionArray);
const processStatusSelectedArray: string[] = [];
const processStatusAllOptionsArray = PROCESS_STATUSES.map(
(processStatusOption: any) => {
const regex = new RegExp(`\\b${processStatusOption}\\b`);
if ((searchParams.get('process_status') || '').match(regex)) {
processStatusSelectedArray.push(processStatusOption);
}
return processStatusOption;
}
);
setProcessStatusSelection(processStatusSelectedArray);
setProcessStatusAllOptions(processStatusAllOptionsArray);
getProcessInstances();
}
// populate process model selection
HttpService.makeCallToBackend({
path: `/process-models?per_page=1000`,
successCallback: processResultForProcessModels,
});
}, [
searchParams,
params,
oneMonthInSeconds,
oneHourInSeconds,
parametersToAlwaysFilterBy,
parametersToGetFromSearchParams,
]);
// does the comparison, but also returns false if either argument
// is not truthy and therefore not comparable.
const isTrueComparison = (param1: any, operation: any, param2: any) => {
if (param1 && param2) {
switch (operation) {
case '<':
return param1 < param2;
case '>':
return param1 > param2;
default:
return false;
}
} else {
return false;
}
};
const applyFilter = (event: any) => {
event.preventDefault();
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
let queryParamString = `per_page=${perPage}&page=${page}`;
const startFromSeconds = convertDateStringToSeconds(startFrom);
const endFromSeconds = convertDateStringToSeconds(endFrom);
const startToSeconds = convertDateStringToSeconds(startTo);
const endToSeconds = convertDateStringToSeconds(endTo);
if (isTrueComparison(startFromSeconds, '>', startToSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "start date to"',
});
return;
}
if (isTrueComparison(endFromSeconds, '>', endToSeconds)) {
setErrorMessage({
message: '"End date from" cannot be after "end date to"',
});
return;
}
if (isTrueComparison(startFromSeconds, '>', endFromSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "end date from"',
});
return;
}
if (isTrueComparison(startToSeconds, '>', endToSeconds)) {
setErrorMessage({
message: '"Start date to" cannot be after "end date to"',
});
return;
}
if (startFromSeconds) {
queryParamString += `&start_from=${startFromSeconds}`;
}
if (startToSeconds) {
queryParamString += `&start_to=${startToSeconds}`;
}
if (endFromSeconds) {
queryParamString += `&end_from=${endFromSeconds}`;
}
if (endToSeconds) {
queryParamString += `&end_to=${endToSeconds}`;
}
if (processStatusSelection.length > 0) {
queryParamString += `&process_status=${processStatusSelection}`;
}
if (processModelSelection) {
queryParamString += `&process_model_identifier=${processModelSelection.id}`;
}
setErrorMessage(null);
navigate(`/admin/process-instances?${queryParamString}`);
};
const dateComponent = (
labelString: any,
name: any,
initialDate: any,
onChangeFunction: any
) => {
return (
<DatePicker dateFormat={DATE_FORMAT_CARBON} datePickerType="single">
<DatePickerInput
id={`date-picker-${name}`}
placeholder={DATE_FORMAT}
labelText={labelString}
type="text"
size="md"
autocomplete="off"
allowInput={false}
onChange={(dateChangeEvent: any) => {
onChangeFunction(dateChangeEvent.srcElement.value);
}}
value={initialDate}
/>
</DatePicker>
);
};
const getSearchParamsAsQueryString = () => {
let queryParamString = '';
Object.keys(parametersToAlwaysFilterBy).forEach((paramName) => {
const searchParamValue = searchParams.get(paramName);
if (searchParamValue) {
queryParamString += `&${paramName}=${searchParamValue}`;
}
});
Object.keys(parametersToGetFromSearchParams).forEach(
(paramName: string) => {
if (searchParams.get(paramName)) {
queryParamString += `&${paramName}=${searchParams.get(paramName)}`;
}
}
);
return queryParamString;
};
const processStatusSearch = () => {
return (
<MultiSelect
label="Choose Status"
className="our-class"
id="process-instance-status-select"
titleText="Status"
items={processStatusAllOptions}
onChange={(selection: any) => {
setProcessStatusSelection(selection.selectedItems);
}}
itemToString={(item: any) => {
return item || '';
}}
selectionFeedback="top-after-reopen"
selectedItems={processStatusSelection}
/>
);
};
const clearFilters = () => {
setProcessModelSelection(null);
setProcessStatusSelection([]);
setStartFrom('');
setStartTo('');
setEndFrom('');
setEndTo('');
};
const filterOptions = () => {
if (!showFilterOptions) {
return null;
}
return (
<>
<Grid fullWidth className="with-bottom-margin">
<Column md={8}>
<ProcessModelSearch
onChange={(selection: any) =>
setProcessModelSelection(selection.selectedItem)
}
processModels={processModelAvailableItems}
selectedItem={processModelSelection}
/>
</Column>
<Column md={8}>{processStatusSearch()}</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
{dateComponent(
'Start date from',
'start-from',
startFrom,
setStartFrom
)}
</Column>
<Column md={4}>
{dateComponent('Start date to', 'start-to', startTo, setStartTo)}
</Column>
<Column md={4}>
{dateComponent('End date from', 'end-from', endFrom, setEndFrom)}
</Column>
<Column md={4}>
{dateComponent('End date to', 'end-to', endTo, setEndTo)}
</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
<ButtonSet>
<Button
kind=""
className="button-white-background"
onClick={clearFilters}
>
Clear
</Button>
<Button
kind="secondary"
onClick={applyFilter}
data-qa="filter-button"
>
Filter
</Button>
</ButtonSet>
</Column>
</Grid>
</>
);
};
const buildTable = () => {
const headerLabels: Record<string, string> = {
id: 'Process Instance Id',
process_model_identifier: 'Process Model',
start_in_seconds: 'Start Time',
end_in_seconds: 'End Time',
status: 'Status',
spiff_step: 'SpiffWorkflow Step',
};
const getHeaderLabel = (header: string) => {
return headerLabels[header] ?? header;
};
const headers = (reportMetadata as any).columns.map((column: any) => {
// return <th>{getHeaderLabel((column as any).Header)}</th>;
return getHeaderLabel((column as any).Header);
});
const formatProcessInstanceId = (row: any, id: any) => {
const modifiedProcessModelId: String = modifyProcessModelPath(
row.process_model_identifier
);
return (
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${row.id}`}
>
{id}
</Link>
);
};
const formatProcessModelIdentifier = (_row: any, identifier: any) => {
return (
<Link
to={`/admin/process-models/${modifyProcessModelPath(identifier)}`}
>
{identifier}
</Link>
);
};
const formatSecondsForDisplay = (_row: any, seconds: any) => {
return convertSecondsToFormattedDate(seconds) || '-';
};
const defaultFormatter = (_row: any, value: any) => {
return value;
};
const columnFormatters: Record<string, any> = {
id: formatProcessInstanceId,
process_model_identifier: formatProcessModelIdentifier,
start_in_seconds: formatSecondsForDisplay,
end_in_seconds: formatSecondsForDisplay,
};
const formattedColumn = (row: any, column: any) => {
const formatter = columnFormatters[column.accessor] ?? defaultFormatter;
const value = row[column.accessor];
if (column.accessor === 'status') {
return (
<td data-qa={`process-instance-status-${value}`}>
{formatter(row, value)}
</td>
);
}
return <td>{formatter(row, value)}</td>;
};
const rows = processInstances.map((row: any) => {
const currentRow = (reportMetadata as any).columns.map((column: any) => {
return formattedColumn(row, column);
});
return <tr key={row.id}>{currentRow}</tr>;
});
return (
<Table size="lg">
<TableHead>
<TableRow>
{headers.map((header: any) => (
<TableHeader key={header}>{header}</TableHeader>
))}
</TableRow>
</TableHead>
<tbody>{rows}</tbody>
</Table>
);
};
const processInstanceBreadcrumbElement = () => {
const processModelFullIdentifier =
getProcessModelFullIdentifierFromSearchParams(searchParams);
@ -496,47 +34,11 @@ export default function ProcessInstanceList() {
const processInstanceTitleElement = () => {
return <h1>Process Instances</h1>;
};
const toggleShowFilterOptions = () => {
setShowFilterOptions(!showFilterOptions);
};
if (pagination) {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
return (
<>
{processInstanceBreadcrumbElement()}
{processInstanceTitleElement()}
<Grid fullWidth>
<Column
sm={{ span: 1, offset: 3 }}
md={{ span: 1, offset: 7 }}
lg={{ span: 1, offset: 15 }}
>
<Button
data-qa="filter-section-expand-toggle"
kind="ghost"
renderIcon={Filter}
iconDescription="Filter Options"
hasIconOnly
size="lg"
onClick={toggleShowFilterOptions}
/>
</Column>
</Grid>
{filterOptions()}
<br />
<PaginationForTable
page={page}
perPage={perPage}
pagination={pagination}
tableToDisplay={buildTable()}
queryParamString={getSearchParamsAsQueryString()}
path="/admin/process-instances"
/>
</>
);
}
return null;
return (
<>
{processInstanceBreadcrumbElement()}
{processInstanceTitleElement()}
<ProcessInstanceListTable />
</>
);
}

View File

@ -6,9 +6,9 @@ import PaginationForTable from '../components/PaginationForTable';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import {
getPageInfoFromSearchParams,
convertSecondsToFormattedDate,
modifyProcessModelPath,
unModifyProcessModelPath,
convertSecondsToFormattedDateTime,
} from '../helpers';
import HttpService from '../services/HttpService';
@ -49,7 +49,7 @@ export default function ProcessInstanceLogList() {
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${rowToUse.process_instance_id}/${rowToUse.spiff_step}`}
>
{convertSecondsToFormattedDate(rowToUse.timestamp)}
{convertSecondsToFormattedDateTime(rowToUse.timestamp)}
</Link>
</td>
</tr>
@ -74,7 +74,6 @@ export default function ProcessInstanceLogList() {
};
if (pagination) {
console.log('params.process_model_id', params.process_model_id);
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
return (
<main>
@ -99,7 +98,6 @@ export default function ProcessInstanceLogList() {
perPage={perPage}
pagination={pagination}
tableToDisplay={buildTable()}
path={`/admin/process-models/${modifiedProcessModelId}/process-instances/${params.process_instance_id}/logs`}
/>
</main>
);

View File

@ -44,9 +44,9 @@ export default function ProcessInstanceReportList() {
const headerStuff = (
<>
<h1>Process Instance Reports</h1>
<h1>Process Instance Perspectives</h1>
<Button href="/admin/process-instances/reports/new">
Add a process instance report
Add a process instance perspective
</Button>
</>
);
@ -61,7 +61,7 @@ export default function ProcessInstanceReportList() {
return (
<main>
{headerStuff}
<p>No reports found</p>
<p>No perspectives found</p>
</main>
);
}

View File

@ -56,7 +56,7 @@ export default function ProcessInstanceReportNew() {
return (
<>
<ProcessBreadcrumb />
<h1>Add Process Model</h1>
<h1>Add Process Instance Perspective</h1>
<form onSubmit={addProcessInstanceReport}>
<label htmlFor="identifier">
identifier:

View File

@ -80,18 +80,17 @@ export default function ProcessInstanceReport() {
processGroupId={params.process_group_id}
linkProcessModel
/>
<h1>Process Instance Report: {params.report_identifier}</h1>
<h1>Process Instance Perspective: {params.report_identifier}</h1>
<Button
href={`/admin/process-instances/reports/${params.report_identifier}/edit`}
>
Edit process instance report
Edit process instance perspective
</Button>
<PaginationForTable
page={page}
perPage={perPage}
pagination={pagination}
tableToDisplay={buildTable()}
path={`/admin/process-models/${params.process_group_id}/${params.process_model_id}/process-instances/report`}
/>
</main>
);

View File

@ -23,6 +23,7 @@ import {
Stack,
// @ts-ignore
} from '@carbon/react';
import { Can } from '@casl/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import HttpService from '../services/HttpService';
import ReactDiagramEditor from '../components/ReactDiagramEditor';
@ -32,6 +33,9 @@ import {
} from '../helpers';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import ErrorContext from '../contexts/ErrorContext';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
export default function ProcessInstanceShow() {
const navigate = useNavigate();
@ -50,6 +54,12 @@ export default function ProcessInstanceShow() {
);
const modifiedProcessModelId = params.process_model_id;
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.messageInstanceListPath]: ['GET'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
const navigateToProcessInstances = (_result: any) => {
navigate(
`/admin/process-instances?process_model_identifier=${unModifiedProcessModelId}`
@ -63,12 +73,12 @@ export default function ProcessInstanceShow() {
});
if (typeof params.spiff_step === 'undefined')
HttpService.makeCallToBackend({
path: `/process-instance/${params.process_instance_id}/tasks?all_tasks=true`,
path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}/tasks?all_tasks=true`,
successCallback: setTasks,
});
else
HttpService.makeCallToBackend({
path: `/process-instance/${params.process_instance_id}/tasks?all_tasks=true&spiff_step=${params.spiff_step}`,
path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}/tasks?all_tasks=true&spiff_step=${params.spiff_step}`,
successCallback: setTasks,
});
}, [params, modifiedProcessModelId]);
@ -245,14 +255,20 @@ export default function ProcessInstanceShow() {
>
Logs
</Button>
<Button
size="sm"
className="button-white-background"
data-qa="process-instance-message-instance-list-link"
href={`/admin/messages?process_model_id=${params.process_model_id}&process_instance_id=${params.process_instance_id}`}
<Can
I="GET"
a={targetUris.messageInstanceListPath}
ability={ability}
>
Messages
</Button>
<Button
size="sm"
className="button-white-background"
data-qa="process-instance-message-instance-list-link"
href={`/admin/messages?process_model_id=${params.process_model_id}&process_instance_id=${params.process_instance_id}`}
>
Messages
</Button>
</Can>
</ButtonSet>
</Column>
</Grid>
@ -521,6 +537,7 @@ export default function ProcessInstanceShow() {
elements.push(resumeButton(processInstanceToUse));
elements.push(
<ButtonWithConfirmation
data-qa="process-instance-delete"
kind="ghost"
renderIcon={TrashCan}
iconDescription="Delete"

View File

@ -18,7 +18,13 @@ import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import HttpService from '../services/HttpService';
import ErrorContext from '../contexts/ErrorContext';
import { makeid, modifyProcessModelPath } from '../helpers';
import { ProcessFile, ProcessModel } from '../interfaces';
import {
CarbonComboBoxProcessSelection,
ProcessFile,
ProcessModel,
ProcessReference,
} from '../interfaces';
import ProcessSearch from '../components/ProcessSearch';
export default function ProcessModelEditDiagram() {
const [showFileNameEditor, setShowFileNameEditor] = useState(false);
@ -36,6 +42,10 @@ export default function ProcessModelEditDiagram() {
const [markdownText, setMarkdownText] = useState<string | undefined>('');
const [markdownEventBus, setMarkdownEventBus] = useState<any>(null);
const [showMarkdownEditor, setShowMarkdownEditor] = useState(false);
const [showProcessSearch, setShowProcessSearch] = useState(false);
const [processSearchEventBus, setProcessSearchEventBus] = useState<any>(null);
const [processSearchElement, setProcessSearchElement] = useState<any>(null);
const [processes, setProcesses] = useState<ProcessReference[]>([]);
const handleShowMarkdownEditor = () => setShowMarkdownEditor(true);
@ -90,6 +100,23 @@ export default function ProcessModelEditDiagram() {
const processModelPath = `process-models/${modifiedProcessModelId}`;
useEffect(() => {
// Grab all available process models in case we need to search for them.
// Taken from the Process Group List
const processResults = (result: any) => {
const selectionArray = result.map((item: any) => {
const label = `${item.display_name} (${item.identifier})`;
Object.assign(item, { label });
return item;
});
setProcesses(selectionArray);
};
HttpService.makeCallToBackend({
path: `/processes`,
successCallback: processResults,
});
}, [processModel]);
useEffect(() => {
const processResult = (result: ProcessModel) => {
setProcessModel(result);
@ -215,6 +242,7 @@ export default function ProcessModelEditDiagram() {
secondaryButtonText="Cancel"
onSecondarySubmit={handleFileNameCancel}
onRequestSubmit={handleFileNameSave}
onRequestClose={handleFileNameCancel}
>
<label>File Name:</label>
<span>
@ -278,7 +306,7 @@ export default function ProcessModelEditDiagram() {
const options: any[] = [];
dmnFiles.forEach((file) => {
file.references.forEach((ref) => {
options.push({ label: ref.name, value: ref.id });
options.push({ label: ref.display_name, value: ref.identifier });
});
});
event.eventBus.fire('spiff.dmn_files.returned', { options });
@ -607,6 +635,7 @@ export default function ProcessModelEditDiagram() {
primaryButtonText="Close"
onRequestSubmit={handleScriptEditorClose}
size="lg"
onRequestClose={handleScriptEditorClose}
>
<Editor
height={500}
@ -644,6 +673,7 @@ export default function ProcessModelEditDiagram() {
modalHeading="Edit Markdown"
primaryButtonText="Close"
onRequestSubmit={handleMarkdownEditorClose}
onRequestClose={handleMarkdownEditorClose}
size="lg"
>
<MDEditor
@ -656,6 +686,45 @@ export default function ProcessModelEditDiagram() {
);
};
const onSearchProcessModels = (
processId: string,
eventBus: any,
element: any
) => {
setProcessSearchEventBus(eventBus);
setProcessSearchElement(element);
setShowProcessSearch(true);
};
const processSearchOnClose = (selection: CarbonComboBoxProcessSelection) => {
const selectedProcessModel = selection.selectedItem;
if (selectedProcessModel) {
processSearchEventBus.fire('spiff.callactivity.update', {
element: processSearchElement,
value: selectedProcessModel.identifier,
});
}
setShowProcessSearch(false);
};
const processModelSelector = () => {
return (
<Modal
open={showProcessSearch}
modalHeading="Select Process Model"
primaryButtonText="Close"
onRequestSubmit={processSearchOnClose}
size="lg"
>
<ProcessSearch
height="500px"
onChange={processSearchOnClose}
processes={processes}
titleText="Process model search"
/>
</Modal>
);
};
const findFileNameForReferenceId = (
id: string,
type: string
@ -676,19 +745,18 @@ export default function ProcessModelEditDiagram() {
return matchFile;
};
/**
* fixme: Not currently in use. This would only work for bpmn files within the process model. Which is right for DMN and json, but not right here. Need to merge in work on the nested process groups before tackling this.
* @param processId
*/
const onLaunchBpmnEditor = (processId: string) => {
const file = findFileNameForReferenceId(processId, 'bpmn');
if (file) {
const processRef = processes.find((p) => {
return p.identifier === processId;
});
if (processRef) {
const path = generatePath(
'/admin/process-models/:process_model_id/files/:file_name',
'/admin/process-models/:process_model_path/files/:file_name',
{
process_model_id: params.process_model_id,
file_name: file.name,
process_model_path: modifyProcessModelPath(
processRef.process_model_id
),
file_name: processRef.file_name,
}
);
window.open(path);
@ -763,6 +831,7 @@ export default function ProcessModelEditDiagram() {
onJsonFilesRequested={onJsonFilesRequested}
onLaunchDmnEditor={onLaunchDmnEditor}
onDmnFilesRequested={onDmnFilesRequested}
onSearchProcessModels={onSearchProcessModels}
/>
);
};
@ -790,7 +859,7 @@ export default function ProcessModelEditDiagram() {
{newFileNameBox()}
{scriptEditor()}
{markdownEditor()}
{processModelSelector()}
<div id="diagram-container" />
</>
);

View File

@ -27,12 +27,26 @@ import {
TableBody,
// @ts-ignore
} from '@carbon/react';
import { Can } from '@casl/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import HttpService from '../services/HttpService';
import ErrorContext from '../contexts/ErrorContext';
import { modifyProcessModelPath, unModifyProcessModelPath } from '../helpers';
import { ProcessFile, ProcessModel, RecentProcessModel } from '../interfaces';
import {
getGroupFromModifiedModelId,
modifyProcessModelPath,
} from '../helpers';
import {
PermissionsToCheck,
ProcessFile,
ProcessInstance,
ProcessModel,
RecentProcessModel,
} from '../interfaces';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
import { usePermissionFetcher } from '../hooks/PermissionService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import ProcessInstanceRun from '../components/ProcessInstanceRun';
const storeRecentProcessModelInLocalStorage = (
processModelForStorage: ProcessModel
@ -88,13 +102,23 @@ export default function ProcessModelShow() {
const setErrorMessage = (useContext as any)(ErrorContext)[1];
const [processModel, setProcessModel] = useState<ProcessModel | null>(null);
const [processInstanceResult, setProcessInstanceResult] = useState(null);
const [processInstance, setProcessInstance] =
useState<ProcessInstance | null>(null);
const [reloadModel, setReloadModel] = useState<boolean>(false);
const [filesToUpload, setFilesToUpload] = useState<any>(null);
const [showFileUploadModal, setShowFileUploadModal] =
useState<boolean>(false);
const navigate = useNavigate();
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processModelShowPath]: ['PUT', 'DELETE'],
[targetUris.processInstanceListPath]: ['GET'],
[targetUris.processInstanceActionPath]: ['POST'],
[targetUris.processModelFileCreatePath]: ['POST', 'GET', 'DELETE'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
const modifiedProcessModelId = modifyProcessModelPath(
`${params.process_model_id}`
);
@ -111,52 +135,19 @@ export default function ProcessModelShow() {
});
}, [reloadModel, modifiedProcessModelId]);
const processModelRun = (processInstance: any) => {
setErrorMessage(null);
HttpService.makeCallToBackend({
path: `/process-instances/${processInstance.id}/run`,
successCallback: setProcessInstanceResult,
failureCallback: setErrorMessage,
httpMethod: 'POST',
});
};
const processInstanceCreateAndRun = () => {
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}/process-instances`,
successCallback: processModelRun,
httpMethod: 'POST',
});
};
const processInstanceResultTag = () => {
if (processModel && processInstanceResult) {
let takeMeToMyTaskBlurb = null;
// FIXME: ensure that the task is actually for the current user as well
const processInstanceId = (processInstanceResult as any).id;
const nextTask = (processInstanceResult as any).next_task;
if (nextTask && nextTask.state === 'READY') {
takeMeToMyTaskBlurb = (
<span>
You have a task to complete. Go to{' '}
<Link to={`/tasks/${processInstanceId}/${nextTask.id}`}>
my task
</Link>
.
</span>
);
}
const processInstanceRunResultTag = () => {
if (processInstance) {
return (
<div className="alert alert-success" role="alert">
<p>
Process Instance {processInstanceId} kicked off (
Process Instance {processInstance.id} kicked off (
<Link
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${processInstanceId}`}
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${processInstance.id}`}
data-qa="process-instance-show-link"
>
view
</Link>
). {takeMeToMyTaskBlurb}
).
</p>
</div>
);
@ -242,6 +233,22 @@ export default function ProcessModelShow() {
return null;
};
const navigateToProcessModels = (_result: any) => {
navigate(
`/admin/process-groups/${getGroupFromModifiedModelId(
modifiedProcessModelId
)}`
);
};
const deleteProcessModel = () => {
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}`,
successCallback: navigateToProcessModels,
httpMethod: 'DELETE',
});
};
const navigateToFileEdit = (processModelFile: ProcessFile) => {
const url = profileModelFileEditUrl(processModelFile);
if (url) {
@ -255,50 +262,62 @@ export default function ProcessModelShow() {
) => {
const elements = [];
elements.push(
<Button
kind="ghost"
renderIcon={Edit}
iconDescription="Edit File"
hasIconOnly
size="lg"
data-qa={`edit-file-${processModelFile.name.replace('.', '-')}`}
onClick={() => navigateToFileEdit(processModelFile)}
/>
<Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
<Button
kind="ghost"
renderIcon={Edit}
iconDescription="Edit File"
hasIconOnly
size="lg"
data-qa={`edit-file-${processModelFile.name.replace('.', '-')}`}
onClick={() => navigateToFileEdit(processModelFile)}
/>
</Can>
);
elements.push(
<Button
kind="ghost"
renderIcon={Download}
iconDescription="Download File"
hasIconOnly
size="lg"
onClick={() => downloadFile(processModelFile.name)}
/>
<Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
<Button
kind="ghost"
renderIcon={Download}
iconDescription="Download File"
hasIconOnly
size="lg"
onClick={() => downloadFile(processModelFile.name)}
/>
</Can>
);
elements.push(
<ButtonWithConfirmation
kind="ghost"
renderIcon={TrashCan}
iconDescription="Delete File"
hasIconOnly
description={`Delete file: ${processModelFile.name}`}
onConfirmation={() => {
onDeleteFile(processModelFile.name);
}}
confirmButtonLabel="Delete"
/>
<Can
I="DELETE"
a={targetUris.processModelFileCreatePath}
ability={ability}
>
<ButtonWithConfirmation
kind="ghost"
renderIcon={TrashCan}
iconDescription="Delete File"
hasIconOnly
description={`Delete file: ${processModelFile.name}`}
onConfirmation={() => {
onDeleteFile(processModelFile.name);
}}
confirmButtonLabel="Delete"
/>
</Can>
);
if (processModelFile.name.match(/\.bpmn$/) && !isPrimaryBpmnFile) {
elements.push(
<Button
kind="ghost"
renderIcon={Favorite}
iconDescription="Set As Primary File"
hasIconOnly
size="lg"
onClick={() => onSetPrimaryFile(processModelFile.name)}
/>
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
<Button
kind="ghost"
renderIcon={Favorite}
iconDescription="Set As Primary File"
hasIconOnly
size="lg"
onClick={() => onSetPrimaryFile(processModelFile.name)}
/>
</Can>
);
}
return elements;
@ -329,7 +348,11 @@ export default function ProcessModelShow() {
let fileLink = null;
const fileUrl = profileModelFileEditUrl(processModelFile);
if (fileUrl) {
fileLink = <Link to={fileUrl}>{processModelFile.name}</Link>;
if (ability.can('GET', targetUris.processModelFileCreatePath)) {
fileLink = <Link to={fileUrl}>{processModelFile.name}</Link>;
} else {
fileLink = <span>{processModelFile.name}</span>;
}
}
constructedTag = (
<TableRow key={processModelFile.name}>
@ -343,7 +366,6 @@ export default function ProcessModelShow() {
return constructedTag;
});
// return <ul>{tags}</ul>;
const headers = ['Name', 'Actions'];
return (
<Table size="lg" useZebraStyles={false}>
@ -361,29 +383,9 @@ export default function ProcessModelShow() {
);
};
const processInstancesUl = () => {
const unmodifiedProcessModelId: String = unModifyProcessModelPath(
`${params.process_model_id}`
);
if (!processModel) {
return null;
}
return (
<ul>
<li>
<Link
to={`/admin/process-instances?process_model_identifier=${unmodifiedProcessModelId}`}
data-qa="process-instance-list-link"
>
List
</Link>
</li>
</ul>
);
};
const handleFileUploadCancel = () => {
setShowFileUploadModal(false);
setFilesToUpload(null);
};
const handleFileUpload = (event: any) => {
@ -401,6 +403,7 @@ export default function ProcessModelShow() {
});
}
setShowFileUploadModal(false);
setFilesToUpload(null);
};
const fileUploadModal = () => {
@ -428,6 +431,7 @@ export default function ProcessModelShow() {
iconDescription="Delete file"
name=""
multiple={false}
onDelete={() => setFilesToUpload(null)}
onChange={(event: any) => setFilesToUpload(event.target.files)}
/>
</Modal>
@ -439,10 +443,11 @@ export default function ProcessModelShow() {
return null;
}
return (
<Grid fullWidth>
<Grid condensed fullWidth>
<Column md={5} lg={9} sm={3}>
<Accordion align="end">
<Accordion align="end" open>
<AccordionItem
open
data-qa="files-accordion"
title={
<Stack orientation="horizontal">
@ -454,47 +459,53 @@ export default function ProcessModelShow() {
</Stack>
}
>
<ButtonSet>
<Button
renderIcon={Upload}
data-qa="upload-file-button"
onClick={() => setShowFileUploadModal(true)}
size="sm"
kind=""
className="button-white-background"
>
Upload File
</Button>
<Button
renderIcon={Add}
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=bpmn`}
size="sm"
>
New BPMN File
</Button>
<Button
renderIcon={Add}
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=dmn`}
size="sm"
>
New DMN File
</Button>
<Button
renderIcon={Add}
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=json`}
size="sm"
>
New JSON File
</Button>
<Button
renderIcon={Add}
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=md`}
size="sm"
>
New Markdown File
</Button>
</ButtonSet>
<br />
<Can
I="POST"
a={targetUris.processModelFileCreatePath}
ability={ability}
>
<ButtonSet>
<Button
renderIcon={Upload}
data-qa="upload-file-button"
onClick={() => setShowFileUploadModal(true)}
size="sm"
kind=""
className="button-white-background"
>
Upload File
</Button>
<Button
renderIcon={Add}
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=bpmn`}
size="sm"
>
New BPMN File
</Button>
<Button
renderIcon={Add}
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=dmn`}
size="sm"
>
New DMN File
</Button>
<Button
renderIcon={Add}
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=json`}
size="sm"
>
New JSON File
</Button>
<Button
renderIcon={Add}
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=md`}
size="sm"
>
New Markdown File
</Button>
</ButtonSet>
<br />
</Can>
{processModelFileList()}
</AccordionItem>
</Accordion>
@ -516,27 +527,57 @@ export default function ProcessModelShow() {
],
]}
/>
<h1>Process Model: {processModel.display_name}</h1>
<Stack orientation="horizontal" gap={1}>
<h1 className="with-icons">
Process Model: {processModel.display_name}
</h1>
<Can I="DELETE" a={targetUris.processModelShowPath} ability={ability}>
<ButtonWithConfirmation
kind="ghost"
renderIcon={TrashCan}
iconDescription="Delete Process Model"
hasIconOnly
description={`Delete process model: ${processModel.display_name}`}
onConfirmation={deleteProcessModel}
confirmButtonLabel="Delete"
/>
</Can>
</Stack>
<p className="process-description">{processModel.description}</p>
<Stack orientation="horizontal" gap={3}>
<Button onClick={processInstanceCreateAndRun} variant="primary">
Run
</Button>
<Button
href={`/admin/process-models/${modifiedProcessModelId}/edit`}
variant="secondary"
<Can
I="POST"
a={targetUris.processInstanceActionPath}
ability={ability}
>
Edit process model
</Button>
<ProcessInstanceRun
processModel={processModel}
onSuccessCallback={setProcessInstance}
/>
</Can>
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
<Button
href={`/admin/process-models/${modifiedProcessModelId}/edit`}
variant="secondary"
>
Edit process model
</Button>
</Can>
</Stack>
<br />
<br />
{processInstanceResultTag()}
{processInstanceRunResultTag()}
<br />
<Can I="GET" a={targetUris.processInstanceListPath} ability={ability}>
<ProcessInstanceListTable
filtersEnabled={false}
processModelFullIdentifier={processModel.id}
perPageOptions={[2, 5, 25]}
/>
<br />
</Can>
{processModelButtons()}
<br />
<br />
<h3>Process Instances</h3>
{processInstancesUl()}
</>
);
}

View File

@ -132,6 +132,7 @@ export default function ReactFormEditor() {
secondaryButtonText="Cancel"
onSecondarySubmit={handleFileNameCancel}
onRequestSubmit={handleFileNameSave}
onRequestClose={handleFileNameCancel}
>
<label>File Name:</label>
<span>
@ -174,6 +175,19 @@ export default function ReactFormEditor() {
<Button onClick={saveFile} variant="danger" data-qa="file-save-button">
Save
</Button>
{params.file_name ? null : (
<Button
onClick={() =>
navigate(
`/admin/process-models/${params.process_model_id}/form-builder`
)
}
variant="danger"
data-qa="form-builder-button"
>
Form Builder
</Button>
)}
{params.file_name ? (
<ButtonWithConfirmation
description={`Delete file ${params.file_name}?`}

View File

@ -42,12 +42,12 @@ export default function SecretList() {
return (
<tr key={(row as any).key}>
<td>
<Link to={`/admin/secrets/${(row as any).key}`}>
<Link to={`/admin/configuration/secrets/${(row as any).key}`}>
{(row as any).id}
</Link>
</td>
<td>
<Link to={`/admin/secrets/${(row as any).key}`}>
<Link to={`/admin/configuration/secrets/${(row as any).key}`}>
{(row as any).key}
</Link>
</td>
@ -83,7 +83,6 @@ export default function SecretList() {
perPage={perPage}
pagination={pagination as any}
tableToDisplay={buildTable()}
path="/admin/secrets"
/>
);
} else {
@ -97,7 +96,7 @@ export default function SecretList() {
<div>
<h1>Secrets</h1>
{SecretsDisplayArea()}
<Button href="/admin/secrets/new">Add a secret</Button>
<Button href="/admin/configuration/secrets/new">Add a secret</Button>
</div>
);
}

View File

@ -12,11 +12,11 @@ export default function SecretNew() {
const navigate = useNavigate();
const navigateToSecret = (_result: any) => {
navigate(`/admin/secrets/${key}`);
navigate(`/admin/configuration/secrets/${key}`);
};
const navigateToSecrets = () => {
navigate(`/admin/secrets`);
navigate(`/admin/configuration/secrets`);
};
const changeSpacesToDash = (someString: string) => {

View File

@ -26,10 +26,6 @@ export default function SecretShow() {
}
};
// const reloadSecret = (_result: any) => {
// window.location.reload();
// };
const updateSecretValue = () => {
if (secret && secretValue) {
secret.value = secretValue;
@ -48,7 +44,7 @@ export default function SecretShow() {
};
const navigateToSecrets = (_result: any) => {
navigate(`/admin/secrets`);
navigate(`/admin/configuration/secrets`);
};
const deleteSecret = () => {

View File

@ -1,13 +1,30 @@
import { useContext, useEffect, useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import Form from '@rjsf/core';
// @ts-ignore
import { Button, Stack } from '@carbon/react';
import { useNavigate, useParams } from 'react-router-dom';
// FIXME: npm install @rjsf/validator-ajv8 and use it as soon as
// rawErrors is fixed.
// https://react-jsonschema-form.readthedocs.io/en/latest/usage/validation/
// https://github.com/rjsf-team/react-jsonschema-form/issues/2309 links to a codesandbox that might be useful to fork
// if we wanted to file a defect against rjsf to show the difference between validator-ajv6 and validator-ajv8.
// https://github.com/rjsf-team/react-jsonschema-form/blob/main/docs/api-reference/uiSchema.md talks about rawErrors
import validator from '@rjsf/validator-ajv6';
import {
TabList,
Tab,
Tabs,
Grid,
Column,
// @ts-ignore
} from '@carbon/react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
// eslint-disable-next-line import/no-named-as-default
import Form from '../themes/carbon';
import HttpService from '../services/HttpService';
import ErrorContext from '../contexts/ErrorContext';
import { modifyProcessModelPath } from '../helpers';
export default function TaskShow() {
const [task, setTask] = useState(null);
@ -18,16 +35,22 @@ export default function TaskShow() {
const setErrorMessage = (useContext as any)(ErrorContext)[1];
useEffect(() => {
const processResult = (result: any) => {
setTask(result);
HttpService.makeCallToBackend({
path: `/process-instances/${modifyProcessModelPath(
result.process_model_identifier
)}/${params.process_instance_id}/tasks`,
successCallback: setUserTasks,
});
};
HttpService.makeCallToBackend({
path: `/tasks/${params.process_instance_id}/${params.task_id}`,
successCallback: setTask,
successCallback: processResult,
// This causes the page to continuously reload
// failureCallback: setErrorMessage,
});
HttpService.makeCallToBackend({
path: `/process-instance/${params.process_instance_id}/tasks`,
successCallback: setUserTasks,
});
}, [params]);
const processSubmitResult = (result: any) => {
@ -56,39 +79,52 @@ export default function TaskShow() {
const buildTaskNavigation = () => {
let userTasksElement;
let selectedTabIndex = 0;
if (userTasks) {
userTasksElement = (userTasks as any).map(function getUserTasksElement(
userTask: any
userTask: any,
index: number
) {
const taskUrl = `/tasks/${params.process_instance_id}/${userTask.id}`;
if (userTask.id === params.task_id) {
return <span>{userTask.name}</span>;
selectedTabIndex = index;
return <Tab selected>{userTask.title}</Tab>;
}
if (userTask.state === 'COMPLETED') {
return (
<Link to={taskUrl} data-qa={`form-nav-${userTask.name}`}>
{userTask.name}
</Link>
<Tab
onClick={() => navigate(taskUrl)}
data-qa={`form-nav-${userTask.name}`}
>
{userTask.title}
</Tab>
);
}
if (userTask.state === 'FUTURE') {
return <span style={{ color: 'red' }}>{userTask.name}</span>;
return <Tab disabled>{userTask.title}</Tab>;
}
if (userTask.state === 'READY') {
return (
<Link to={taskUrl} data-qa={`form-nav-${userTask.name}`}>
{userTask.name} - Current
</Link>
<Tab
onClick={() => navigate(taskUrl)}
data-qa={`form-nav-${userTask.name}`}
>
{userTask.title}
</Tab>
);
}
return null;
});
}
return (
<Stack orientation="horizontal" gap={3}>
<Button href="/tasks">Go Back To List</Button>
{userTasksElement}
</Stack>
<Tabs
title="Steps in this process instance involving people"
selectedIndex={selectedTabIndex}
>
<TabList aria-label="List of tabs" contained>
{userTasksElement}
</TabList>
</Tabs>
);
};
@ -132,14 +168,19 @@ export default function TaskShow() {
}
return (
<Form
formData={taskData}
onSubmit={handleFormSubmit}
schema={jsonSchema}
uiSchema={formUiSchema}
>
{reactFragmentToHideSubmitButton}
</Form>
<Grid fullWidth condensed>
<Column md={5} lg={8} sm={4}>
<Form
formData={taskData}
onSubmit={handleFormSubmit}
schema={jsonSchema}
uiSchema={formUiSchema}
validator={validator}
>
{reactFragmentToHideSubmitButton}
</Form>
</Column>
</Grid>
);
};
@ -157,7 +198,7 @@ export default function TaskShow() {
);
};
if (task) {
if (task && userTasks) {
const taskToUse = task as any;
let statusString = '';
if (taskToUse.state !== 'READY') {

View File

@ -0,0 +1,17 @@
import React from 'react';
import AddIcon from '@mui/icons-material/Add';
import IconButton from '@mui/material/IconButton';
import { IconButtonProps } from '@rjsf/utils';
const AddButton: React.ComponentType<IconButtonProps> = ({
uiSchema,
...props
}) => {
return (
<IconButton title="Add Item" {...props} color="primary">
<AddIcon />
</IconButton>
);
};
export default AddButton;

View File

@ -0,0 +1,2 @@
export { default } from './AddButton';
export * from './AddButton';

View File

@ -0,0 +1,72 @@
import React, { CSSProperties } from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Paper from '@mui/material/Paper';
import { ArrayFieldTemplateItemType } from '@rjsf/utils';
function ArrayFieldItemTemplate(props: ArrayFieldTemplateItemType) {
const {
children,
disabled,
hasToolbar,
hasMoveDown,
hasMoveUp,
hasRemove,
index,
onDropIndexClick,
onReorderClick,
readonly,
uiSchema,
registry,
} = props;
const { MoveDownButton, MoveUpButton, RemoveButton } =
registry.templates.ButtonTemplates;
const btnStyle: CSSProperties = {
flex: 1,
paddingLeft: 6,
paddingRight: 6,
fontWeight: 'bold',
minWidth: 0,
};
return (
<Grid container alignItems="center">
<Grid item xs style={{ overflow: 'auto' }}>
<Box mb={2}>
<Paper elevation={2}>
<Box p={2}>{children}</Box>
</Paper>
</Box>
</Grid>
{hasToolbar && (
<Grid item>
{(hasMoveUp || hasMoveDown) && (
<MoveUpButton
style={btnStyle}
disabled={disabled || readonly || !hasMoveUp}
onClick={onReorderClick(index, index - 1)}
uiSchema={uiSchema}
/>
)}
{(hasMoveUp || hasMoveDown) && (
<MoveDownButton
style={btnStyle}
disabled={disabled || readonly || !hasMoveDown}
onClick={onReorderClick(index, index + 1)}
uiSchema={uiSchema}
/>
)}
{hasRemove && (
<RemoveButton
style={btnStyle}
disabled={disabled || readonly}
onClick={onDropIndexClick(index)}
uiSchema={uiSchema}
/>
)}
</Grid>
)}
</Grid>
);
}
export default ArrayFieldItemTemplate;

View File

@ -0,0 +1,2 @@
export { default } from './ArrayFieldItemTemplate';
export * from './ArrayFieldItemTemplate';

View File

@ -0,0 +1,90 @@
import React from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Paper from '@mui/material/Paper';
import {
ArrayFieldTemplateItemType,
ArrayFieldTemplateProps,
getTemplate,
getUiOptions,
} from '@rjsf/utils';
function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
const {
canAdd,
disabled,
idSchema,
uiSchema,
items,
onAddClick,
readonly,
registry,
required,
schema,
title,
} = props;
const uiOptions = getUiOptions(uiSchema);
const ArrayFieldDescriptionTemplate =
getTemplate<'ArrayFieldDescriptionTemplate'>(
'ArrayFieldDescriptionTemplate',
registry,
uiOptions
);
const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate'>(
'ArrayFieldItemTemplate',
registry,
uiOptions
);
const ArrayFieldTitleTemplate = getTemplate<'ArrayFieldTitleTemplate'>(
'ArrayFieldTitleTemplate',
registry,
uiOptions
);
// Button templates are not overridden in the uiSchema
const {
ButtonTemplates: { AddButton },
} = registry.templates;
return (
<Paper elevation={2}>
<Box p={2}>
<ArrayFieldTitleTemplate
idSchema={idSchema}
title={uiOptions.title || title}
schema={schema}
uiSchema={uiSchema}
required={required}
registry={registry}
/>
<ArrayFieldDescriptionTemplate
idSchema={idSchema}
description={uiOptions.description || schema.description}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
<Grid container key={`array-item-list-${idSchema.$id}`}>
{items &&
items.map(({ key, ...itemProps }: ArrayFieldTemplateItemType) => (
<ArrayFieldItemTemplate key={key} {...itemProps} />
))}
{canAdd && (
<Grid container justifyContent="flex-end">
<Grid item>
<Box mt={2}>
<AddButton
className="array-item-add"
onClick={onAddClick}
disabled={disabled || readonly}
uiSchema={uiSchema}
/>
</Box>
</Grid>
</Grid>
)}
</Grid>
</Box>
</Paper>
);
}
export default ArrayFieldTemplate;

View File

@ -0,0 +1,2 @@
export { default } from './ArrayFieldTemplate';
export * from './ArrayFieldTemplate';

View File

@ -0,0 +1,126 @@
// @ts-ignore
import { TextInput } from '@carbon/react';
import {
getInputProps,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
WidgetProps,
} from '@rjsf/utils';
import { useCallback } from 'react';
/** The `BaseInputTemplate` is the template to use to render the basic `<input>` component for the `core` theme.
* It is used as the template for rendering many of the <input> based widgets that differ by `type` and callbacks only.
* It can be customized/overridden for other themes or individual implementations as needed.
*
* @param props - The `WidgetProps` for this template
*/
export default function BaseInputTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: WidgetProps<T, S, F>) {
const {
id,
value,
readonly,
disabled,
autofocus,
label,
onBlur,
onFocus,
onChange,
required,
options,
schema,
uiSchema,
formContext,
registry,
rawErrors,
type,
...rest
} = props;
// Note: since React 15.2.0 we can't forward unknown element attributes, so we
// exclude the "options" and "schema" ones here.
if (!id) {
console.log('No id for', props);
throw new Error(`no id for props ${JSON.stringify(props)}`);
}
const inputProps = {
...rest,
...getInputProps<T, S, F>(schema, type, options),
};
let inputValue;
if (inputProps.type === 'number' || inputProps.type === 'integer') {
inputValue = value || value === 0 ? value : '';
} else {
inputValue = value == null ? '' : value;
}
const _onChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) =>
onChange(value === '' ? options.emptyValue : value),
[onChange, options]
);
const _onBlur = useCallback(
({ target: { value } }: React.FocusEvent<HTMLInputElement>) =>
onBlur(id, value),
[onBlur, id]
);
const _onFocus = useCallback(
({ target: { value } }: React.FocusEvent<HTMLInputElement>) =>
onFocus(id, value),
[onFocus, id]
);
let labelToUse = label;
if (uiSchema && uiSchema['ui:title']) {
labelToUse = uiSchema['ui:title'];
} else if (schema && schema.title) {
labelToUse = schema.title;
}
if (required) {
labelToUse = `${labelToUse}*`;
}
let invalid = false;
let errorMessageForField = null;
if (rawErrors && rawErrors.length > 0) {
invalid = true;
errorMessageForField = `${labelToUse.replace(/\*$/, '')} ${rawErrors[0]}`;
}
return (
<>
<TextInput
id={id}
name={id}
labelText={labelToUse}
invalid={invalid}
invalidText={errorMessageForField}
autoFocus={autofocus}
disabled={disabled || readonly}
value={value || value === 0 ? value : ''}
onChange={_onChange}
onBlur={_onBlur}
onFocus={_onFocus}
// eslint-disable-next-line react/jsx-props-no-spreading
{...inputProps}
/>
{Array.isArray(schema.examples) && (
<datalist key={`datalist_${id}`} id={`examples_${id}`}>
{[
...new Set(
schema.examples.concat(schema.default ? [schema.default] : [])
),
].map((example: any) => (
<option key={example} value={example} />
))}
</datalist>
)}
</>
);
}

View File

@ -0,0 +1,2 @@
export { default } from './BaseInputTemplate';
export * from './BaseInputTemplate';

View File

@ -0,0 +1,8 @@
import { ComponentType } from 'react';
import { withTheme, FormProps } from '@rjsf/core';
import Theme from '../Theme';
const CarbonForm: ComponentType<FormProps> = withTheme(Theme);
export default CarbonForm;

View File

@ -0,0 +1,2 @@
export { default } from './CarbonForm';
export * from './CarbonForm';

View File

@ -0,0 +1,52 @@
import React from 'react';
import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
import { schemaRequiresTrueValue, WidgetProps } from '@rjsf/utils';
function CheckboxWidget(props: WidgetProps) {
const {
schema,
id,
value,
disabled,
readonly,
label,
autofocus,
onChange,
onBlur,
onFocus,
} = props;
// Because an unchecked checkbox will cause html5 validation to fail, only add
// the "required" attribute if the field value must be "true", due to the
// "const" or "enum" keywords
const required = schemaRequiresTrueValue(schema);
const _onChange = (_: any, checked: boolean) => onChange(checked);
const _onBlur = ({
target: { value },
}: React.FocusEvent<HTMLButtonElement>) => onBlur(id, value);
const _onFocus = ({
target: { value },
}: React.FocusEvent<HTMLButtonElement>) => onFocus(id, value);
return (
<FormControlLabel
control={
<Checkbox
id={id}
name={id}
checked={typeof value === 'undefined' ? false : Boolean(value)}
required={required}
disabled={disabled || readonly}
autoFocus={autofocus}
onChange={_onChange}
onBlur={_onBlur}
onFocus={_onFocus}
/>
}
label={label || ''}
/>
);
}
export default CheckboxWidget;

View File

@ -0,0 +1,2 @@
export { default } from './CheckboxWidget';
export * from './CheckboxWidget';

View File

@ -0,0 +1,93 @@
import React from 'react';
import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormGroup from '@mui/material/FormGroup';
import FormLabel from '@mui/material/FormLabel';
import { WidgetProps } from '@rjsf/utils';
const selectValue = (value: any, selected: any, all: any) => {
const at = all.indexOf(value);
const updated = selected.slice(0, at).concat(value, selected.slice(at));
// As inserting values at predefined index positions doesn't work with empty
// arrays, we need to reorder the updated selection to match the initial order
return updated.sort((a: any, b: any) => all.indexOf(a) > all.indexOf(b));
};
const deselectValue = (value: any, selected: any) => {
return selected.filter((v: any) => v !== value);
};
function CheckboxesWidget({
schema,
label,
id,
disabled,
options,
value,
autofocus,
readonly,
required,
onChange,
onBlur,
onFocus,
}: WidgetProps) {
const { enumOptions, enumDisabled, inline } = options;
const _onChange =
(option: any) =>
({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) => {
const all = (enumOptions as any).map(({ value }: any) => value);
if (checked) {
onChange(selectValue(option.value, value, all));
} else {
onChange(deselectValue(option.value, value));
}
};
const _onBlur = ({
target: { value },
}: React.FocusEvent<HTMLButtonElement>) => onBlur(id, value);
const _onFocus = ({
target: { value },
}: React.FocusEvent<HTMLButtonElement>) => onFocus(id, value);
return (
<>
<FormLabel required={required} htmlFor={id}>
{label || schema.title}
</FormLabel>
<FormGroup id={id} row={!!inline}>
{Array.isArray(enumOptions) &&
enumOptions.map((option, index: number) => {
const checked = value.indexOf(option.value) !== -1;
const itemDisabled =
Array.isArray(enumDisabled) &&
enumDisabled.indexOf(option.value) !== -1;
const checkbox = (
<Checkbox
id={`${id}-${option.value}`}
name={id}
checked={checked}
disabled={disabled || itemDisabled || readonly}
autoFocus={autofocus && index === 0}
onChange={_onChange(option)}
onBlur={_onBlur}
onFocus={_onFocus}
/>
);
return (
<FormControlLabel
control={checkbox}
key={option.value}
label={option.label}
/>
);
})}
</FormGroup>
</>
);
}
export default CheckboxesWidget;

View File

@ -0,0 +1,2 @@
export { default } from './CheckboxesWidget';
export * from './CheckboxesWidget';

View File

@ -0,0 +1,29 @@
import React from 'react';
import { getTemplate, localToUTC, utcToLocal, WidgetProps } from '@rjsf/utils';
function DateTimeWidget(props: WidgetProps) {
const { options, registry } = props;
const BaseInputTemplate = getTemplate<'BaseInputTemplate'>(
'BaseInputTemplate',
registry,
options
);
const value = utcToLocal(props.value);
const onChange = (value: any) => {
props.onChange(localToUTC(value));
};
return (
<BaseInputTemplate
type="datetime-local"
InputLabelProps={{
shrink: true,
}}
{...props}
value={value}
onChange={onChange}
/>
);
}
export default DateTimeWidget;

View File

@ -0,0 +1,2 @@
export { default } from './DateTimeWidget';
export * from './DateTimeWidget';

View File

@ -0,0 +1,22 @@
import React from 'react';
import { getTemplate, WidgetProps } from '@rjsf/utils';
function DateWidget(props: WidgetProps) {
const { options, registry } = props;
const BaseInputTemplate = getTemplate<'BaseInputTemplate'>(
'BaseInputTemplate',
registry,
options
);
return (
<BaseInputTemplate
type="date"
InputLabelProps={{
shrink: true,
}}
{...props}
/>
);
}
export default DateWidget;

View File

@ -0,0 +1,2 @@
export { default } from './DateWidget';
export * from './DateWidget';

View File

@ -0,0 +1,17 @@
import React from 'react';
import Typography from '@mui/material/Typography';
import { DescriptionFieldProps } from '@rjsf/utils';
function DescriptionField({ description, id }: DescriptionFieldProps) {
if (description) {
return (
<Typography id={id} variant="subtitle2" style={{ marginTop: '5px' }}>
{description}
</Typography>
);
}
return null;
}
export default DescriptionField;

View File

@ -0,0 +1,2 @@
export { default } from './DescriptionField';
export * from './DescriptionField';

View File

@ -0,0 +1,16 @@
import { ErrorListProps } from '@rjsf/utils';
// @ts-ignore
import { Tag } from '@carbon/react';
function ErrorList({ errors }: ErrorListProps) {
if (errors) {
return (
<Tag type="red" size="md" title="Fill Required Fields">
Please fill out required fields
</Tag>
);
}
return null;
}
export default ErrorList;

View File

@ -0,0 +1,2 @@
export { default } from './ErrorList';
export * from './ErrorList';

View File

@ -0,0 +1,9 @@
import { FieldErrorProps } from '@rjsf/utils';
/** The `FieldErrorTemplate` component renders the errors local to the particular field
*
* @param props - The `FieldErrorProps` for the errors being rendered
*/
export default function FieldErrorTemplate(_props: FieldErrorProps) {
return null;
}

View File

@ -0,0 +1,2 @@
export { default } from './FieldErrorTemplate';
export * from './FieldErrorTemplate';

View File

@ -0,0 +1,16 @@
import React from 'react';
import { FieldHelpProps } from '@rjsf/utils';
import FormHelperText from '@mui/material/FormHelperText';
/** The `FieldHelpTemplate` component renders any help desired for a field
*
* @param props - The `FieldHelpProps` to be rendered
*/
export default function FieldHelpTemplate(props: FieldHelpProps) {
const { idSchema, help } = props;
if (!help) {
return null;
}
const id = `${idSchema.$id}__help`;
return <FormHelperText id={id}>{help}</FormHelperText>;
}

View File

@ -0,0 +1,2 @@
export { default } from './FieldHelpTemplate';
export * from './FieldHelpTemplate';

View File

@ -0,0 +1,64 @@
import React from 'react';
import FormControl from '@mui/material/FormControl';
import Typography from '@mui/material/Typography';
import { FieldTemplateProps, getTemplate, getUiOptions } from '@rjsf/utils';
function FieldTemplate({
id,
children,
classNames,
disabled,
displayLabel,
hidden,
label,
onDropPropertyClick,
onKeyChange,
readonly,
required,
rawErrors = [],
errors,
help,
rawDescription,
schema,
uiSchema,
registry,
}: FieldTemplateProps) {
const uiOptions = getUiOptions(uiSchema);
const WrapIfAdditionalTemplate = getTemplate<'WrapIfAdditionalTemplate'>(
'WrapIfAdditionalTemplate',
registry,
uiOptions
);
if (hidden) {
return <div style={{ display: 'none' }}>{children}</div>;
}
return (
<WrapIfAdditionalTemplate
classNames={classNames}
disabled={disabled}
id={id}
label={label}
onDropPropertyClick={onDropPropertyClick}
onKeyChange={onKeyChange}
readonly={readonly}
required={required}
schema={schema}
uiSchema={uiSchema}
registry={registry}
>
<FormControl fullWidth error={!!rawErrors.length} required={required}>
{children}
{displayLabel && rawDescription ? (
<Typography variant="caption" color="textSecondary">
{rawDescription}
</Typography>
) : null}
{errors}
{help}
</FormControl>
</WrapIfAdditionalTemplate>
);
}
export default FieldTemplate;

View File

@ -0,0 +1,2 @@
export { default } from './FieldTemplate';
export * from './FieldTemplate';

View File

@ -0,0 +1,55 @@
import React from 'react';
import IconButton, {
IconButtonProps as MuiIconButtonProps,
} from '@mui/material/IconButton';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import RemoveIcon from '@mui/icons-material/Remove';
import { IconButtonProps } from '@rjsf/utils';
export default function MuiIconButton(props: IconButtonProps) {
const { icon, color, uiSchema, ...otherProps } = props;
return (
<IconButton
{...otherProps}
size="small"
color={color as MuiIconButtonProps['color']}
>
{icon}
</IconButton>
);
}
export function MoveDownButton(props: IconButtonProps) {
return (
<MuiIconButton
title="Move down"
{...props}
icon={<ArrowDownwardIcon fontSize="small" />}
/>
);
}
export function MoveUpButton(props: IconButtonProps) {
return (
<MuiIconButton
title="Move up"
{...props}
icon={<ArrowUpwardIcon fontSize="small" />}
/>
);
}
export function RemoveButton(props: IconButtonProps) {
const { iconType, ...otherProps } = props;
return (
<MuiIconButton
title="Remove"
{...otherProps}
color="error"
icon={
<RemoveIcon fontSize={iconType === 'default' ? undefined : 'small'} />
}
/>
);
}

View File

@ -0,0 +1,2 @@
export { default } from './IconButton';
export * from './IconButton';

View File

@ -0,0 +1,89 @@
import React from 'react';
import Grid from '@mui/material/Grid';
import {
ObjectFieldTemplateProps,
canExpand,
getTemplate,
getUiOptions,
} from '@rjsf/utils';
function ObjectFieldTemplate({
description,
title,
properties,
required,
disabled,
readonly,
uiSchema,
idSchema,
schema,
formData,
onAddClick,
registry,
}: ObjectFieldTemplateProps) {
const uiOptions = getUiOptions(uiSchema);
const TitleFieldTemplate = getTemplate<'TitleFieldTemplate'>(
'TitleFieldTemplate',
registry,
uiOptions
);
const DescriptionFieldTemplate = getTemplate<'DescriptionFieldTemplate'>(
'DescriptionFieldTemplate',
registry,
uiOptions
);
// Button templates are not overridden in the uiSchema
const {
ButtonTemplates: { AddButton },
} = registry.templates;
return (
<>
{(uiOptions.title || title) && (
<TitleFieldTemplate
id={`${idSchema.$id}-title`}
title={title}
required={required}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
)}
{(uiOptions.description || description) && (
<DescriptionFieldTemplate
id={`${idSchema.$id}-description`}
description={uiOptions.description || description!}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
)}
<Grid container spacing={2} style={{ marginTop: '10px' }}>
{properties.map((element, index) =>
// Remove the <Grid> if the inner element is hidden as the <Grid>
// itself would otherwise still take up space.
element.hidden ? (
element.content
) : (
<Grid item xs={12} key={index} style={{ marginBottom: '10px' }}>
{element.content}
</Grid>
)
)}
{canExpand(schema, uiSchema, formData) && (
<Grid container justifyContent="flex-end">
<Grid item>
<AddButton
className="object-property-expand"
onClick={onAddClick(schema)}
disabled={disabled || readonly}
uiSchema={uiSchema}
/>
</Grid>
</Grid>
)}
</Grid>
</>
);
}
export default ObjectFieldTemplate;

View File

@ -0,0 +1,2 @@
export { default } from './ObjectFieldTemplate';
export * from './ObjectFieldTemplate';

View File

@ -0,0 +1,75 @@
import React from "react";
import FormControlLabel from "@mui/material/FormControlLabel";
import FormLabel from "@mui/material/FormLabel";
import Radio from "@mui/material/Radio";
import RadioGroup from "@mui/material/RadioGroup";
import { WidgetProps } from "@rjsf/utils";
const RadioWidget = ({
id,
schema,
options,
value,
required,
disabled,
readonly,
label,
onChange,
onBlur,
onFocus,
}: WidgetProps) => {
const { enumOptions, enumDisabled } = options;
const _onChange = (_: any, value: any) =>
onChange(schema.type == "boolean" ? value !== "false" : value);
const _onBlur = ({ target: { value } }: React.FocusEvent<HTMLInputElement>) =>
onBlur(id, value);
const _onFocus = ({
target: { value },
}: React.FocusEvent<HTMLInputElement>) => onFocus(id, value);
const row = options ? options.inline : false;
return (
<>
<FormLabel required={required} htmlFor={id}>
{label || schema.title}
</FormLabel>
<RadioGroup
id={id}
name={id}
value={`${value}`}
row={row as boolean}
onChange={_onChange}
onBlur={_onBlur}
onFocus={_onFocus}
>
{Array.isArray(enumOptions) &&
enumOptions.map((option) => {
const itemDisabled =
Array.isArray(enumDisabled) &&
enumDisabled.indexOf(option.value) !== -1;
const radio = (
<FormControlLabel
control={
<Radio
name={id}
id={`${id}-${option.value}`}
color="primary"
/>
}
label={`${option.label}`}
value={`${option.value}`}
key={option.value}
disabled={disabled || itemDisabled || readonly}
/>
);
return radio;
})}
</RadioGroup>
</>
);
};
export default RadioWidget;

View File

@ -0,0 +1,2 @@
export { default } from './RadioWidget';
export * from './RadioWidget';

View File

@ -0,0 +1,47 @@
import React from 'react';
import FormLabel from '@mui/material/FormLabel';
import Slider from '@mui/material/Slider';
import { WidgetProps, rangeSpec } from '@rjsf/utils';
function RangeWidget({
value,
readonly,
disabled,
onBlur,
onFocus,
options,
schema,
onChange,
required,
label,
id,
}: WidgetProps) {
const sliderProps = { value, label, id, name: id, ...rangeSpec(schema) };
const _onChange = (_: any, value?: number | number[]) => {
onChange(value ? options.emptyValue : value);
};
const _onBlur = ({ target: { value } }: React.FocusEvent<HTMLInputElement>) =>
onBlur(id, value);
const _onFocus = ({
target: { value },
}: React.FocusEvent<HTMLInputElement>) => onFocus(id, value);
return (
<>
<FormLabel required={required} id={id}>
{label}
</FormLabel>
<Slider
disabled={disabled || readonly}
onChange={_onChange}
onBlur={_onBlur}
onFocus={_onFocus}
valueLabelDisplay="auto"
{...sliderProps}
/>
</>
);
}
export default RangeWidget;

View File

@ -0,0 +1,2 @@
export { default } from './RangeWidget';
export * from './RangeWidget';

View File

@ -0,0 +1,87 @@
// @ts-ignore
import { Select, SelectItem } from '@carbon/react';
import { WidgetProps, processSelectValue } from '@rjsf/utils';
function SelectWidget({
schema,
id,
options,
label,
required,
disabled,
readonly,
value,
multiple,
autofocus,
onChange,
onBlur,
onFocus,
uiSchema,
placeholder,
rawErrors = [],
}: WidgetProps) {
const { enumOptions, enumDisabled } = options;
const emptyValue = multiple ? [] : '';
const _onChange = ({
target: { value },
}: React.ChangeEvent<{ name?: string; value: unknown }>) =>
onChange(processSelectValue(schema, value, options));
const _onBlur = ({ target: { value } }: React.FocusEvent<HTMLInputElement>) =>
onBlur(id, processSelectValue(schema, value, options));
const _onFocus = ({
target: { value },
}: React.FocusEvent<HTMLInputElement>) =>
onFocus(id, processSelectValue(schema, value, options));
let labelToUse = label;
if (uiSchema && uiSchema['ui:title']) {
labelToUse = uiSchema['ui:title'];
} else if (schema && schema.title) {
labelToUse = schema.title;
}
if (required) {
labelToUse = `${labelToUse}*`;
}
let invalid = false;
let errorMessageForField = null;
if (rawErrors && rawErrors.length > 0) {
invalid = true;
errorMessageForField = `${labelToUse.replace(/\*$/, '')} ${rawErrors[0]}`;
}
return (
<Select
id={id}
name={id}
labelText={labelToUse}
select
helperText={placeholder}
value={typeof value === 'undefined' ? emptyValue : value}
disabled={disabled || readonly}
autoFocus={autofocus}
error={rawErrors.length > 0}
onChange={_onChange}
onBlur={_onBlur}
onFocus={_onFocus}
invalid={invalid}
invalidText={errorMessageForField}
InputLabelProps={{
shrink: true,
}}
SelectProps={{
multiple: typeof multiple === 'undefined' ? false : multiple,
}}
>
{(enumOptions as any).map(({ value, label }: any, i: number) => {
const disabled: any =
enumDisabled && (enumDisabled as any).indexOf(value) != -1;
return <SelectItem text={label} value={value} disabled={disabled} />;
})}
</Select>
);
}
export default SelectWidget;

View File

@ -0,0 +1,2 @@
export { default } from './SelectWidget';
export * from './SelectWidget';

View File

@ -0,0 +1,30 @@
import React from 'react';
// import Box from '@mui/material/Box';
// @ts-ignore
import { Button } from '@carbon/react';
import { SubmitButtonProps, getSubmitButtonOptions } from '@rjsf/utils';
// const SubmitButton: React.ComponentType<SubmitButtonProps> = (props) => {
function SubmitButton(props: SubmitButtonProps) {
const { uiSchema } = props;
const {
submitText,
norender,
props: submitButtonProps,
} = getSubmitButtonOptions(uiSchema);
if (norender) {
return null;
}
return (
<Button
className="react-json-schema-form-submit-button"
type="submit"
// eslint-disable-next-line react/jsx-props-no-spreading
{...submitButtonProps}
>
{submitText}
</Button>
);
}
export default SubmitButton;

View File

@ -0,0 +1,2 @@
export { default } from './SubmitButton';
export * from './SubmitButton';

View File

@ -0,0 +1,35 @@
import AddButton from '../AddButton';
import ArrayFieldItemTemplate from '../ArrayFieldItemTemplate';
import ArrayFieldTemplate from '../ArrayFieldTemplate';
import BaseInputTemplate from '../BaseInputTemplate';
import DescriptionField from '../DescriptionField';
import ErrorList from '../ErrorList';
import { MoveDownButton, MoveUpButton, RemoveButton } from '../IconButton';
import FieldErrorTemplate from '../FieldErrorTemplate';
import FieldHelpTemplate from '../FieldHelpTemplate';
import FieldTemplate from '../FieldTemplate';
import ObjectFieldTemplate from '../ObjectFieldTemplate';
import SubmitButton from '../SubmitButton';
import TitleField from '../TitleField';
import WrapIfAdditionalTemplate from '../WrapIfAdditionalTemplate';
export default {
ArrayFieldItemTemplate,
ArrayFieldTemplate,
BaseInputTemplate,
ButtonTemplates: {
AddButton,
MoveDownButton,
MoveUpButton,
RemoveButton,
SubmitButton,
},
DescriptionFieldTemplate: DescriptionField,
ErrorListTemplate: ErrorList,
FieldErrorTemplate,
FieldHelpTemplate,
FieldTemplate,
ObjectFieldTemplate,
TitleFieldTemplate: TitleField,
WrapIfAdditionalTemplate,
};

Some files were not shown because too many files have changed in this diff Show More