Squashed 'spiffworkflow-frontend/' changes from 326040b3c..55607af93

55607af93 fixed broken test w/ burnettk
4ea766eb4 mypy w/ burnettk cullerton
fd2239b0e added git creds for pushing on publish w/ burnettk cullerton
0281bec01 added new notification component that allows links based on carbons w/ burnettk cullerton
49190128c display URL to open PR *** Need to figure out how to turn this into a link ***
72b15c52c Return message to use on successful publish
4997375c8 Merge branch 'main' into feature/git-integration
39deda4d4 Merge branch 'main' into feature/git-integration
027dae1c6 First pass at git integration
db0c8dc29 break process instance log list page into two tabs, simple and detailed
d9df1104c get the columsn for the instance list table anytime filter options are displayed if empty
3792dafdb make the frontend uris match the api calls better w/ burnettk
7095e4723 more api cleanup w/ burnettk
c514ac656 cleaned up more api routes for permissions w/ burnettk
c758216ed updated tasks endpoint to task-data for easier permission setting w/ burnettk
7504e1857 pyl w/ burnettk
b7edc501a Merge remote-tracking branch 'origin/main' into new_report
112eed7f3 some updates to fix up saving perspectives w/ burnettk
6da6ebe2d Use the identifier, not the id when locating a process model or dmn table.
51515ea21 using an array for metadata extraction paths now instead of dictionaries w/ burnettk
f0b8e7185 added some support to add process model metadata. need to fix frontend w/ burnettk
0777bda31 filtering by metadata works w/ burnettk
d82a00018 favor report id over identifier but support both and ui updates to allow setting a condition value on a metadata field, changing the display name, and fixes for saving and updating a report
de218ba8e updated column form var w/ burnettk
de38dc436 added ability to update the display name for perspective columns w/ burnettk
555360eb6 some updates for process instance reports and metadata w/ burnettk
f0f4dcd89 better display for failure causes on message list w/ burnettk
c4faf5d55 added correlations to message list table w/ burnettk
65feaeecf Merge remote-tracking branch 'origin/main' into new_report
fe9dddc03 Choose new report
f20d6ee75 Save dates
b55a24a1c Save first status
c47c62a6d added script to save process instance metadata and fixed permissions issue w/ burnettk cullerton
a0098ebd9 Save selected process model
55ecbe565 Use current columns
6de52904e WIP
bacf11bdc Save as report component
472578b99 adding the username to the report tables
bef4add43 allow disabling the permission check for the Create New Instance page to improve performance.
ab929fcaa Merge branch 'main' of github.com:sartography/spiff-arena into main
603db83cb "Continue" rather than "Submit" when displaying manual tasks.
5db42f0e0 Processes you can start is now: Processes I can start
c5c6c0fac lint
6f0c58da8 Auto Reload the Process Lists on the home pages' in-progress, and complete tabs
54e8a4717 update bpmn-js-spiffworkflow with better data-object handling
a72daa441 Clean up css for the filter icon
c755889ae update wording per harmeet: Tasks for my open processes is now My open instances
bda4a6ee3 heading for instances on model show page, move instances below files, add margins
21a3eea47 display name instead of id, margin under table sections, Download xml to Download
1d83e3ac1 do not mislead user about being able to edit and clean up time in words
07380eec7 auto refresh tasks waiting for my groups on homepage
710d2340a time ago in words for in progress tab per harmeet feedback
88c4be1bd put id before process like completed tab and add title text to explain what is happening
fb4136892 use process model display name rather than id for completed instances tab
8031fda3a left align files section with Start button per harmeet feedback
c339d7dec add fin1, lead1, and Tasks actioned by me to Tasks completed by me
951c21f39 improve wording
ed38b57e8 consistency is key
e09373027 remove View label next to process instance id
38d20ceab ui feedback
3c0284633 some ui changes w/ burnettk
e3711f4fd updated copmleted table text w/ burnettk
0688f5ec1 updated instances table descriptions w/ burnettk
e9e9b8e2e added descriptions to task tables w/ burnettk
9b1d61866 updated breadcrumb to use display name w/ burnettk
a9895f472 Hide perspectives link in nav bar (#59)
77390519b rename process_groups_list to process_group_list and fix lint
31bb0facd some updates to ui homepage to align more with notion doc
12e719146 fixed cypress tests
476c19f72 fix typo
b266273e4 some more perm updates for core user w/ burnettk
05161fbcb Start of system report filters (#57)
f0e0732ab fixed editing a process model w/ burnettk
b02b5a2e4 filter process models based on user permissions on the backend if specified w/ burnettk
29093932f use tiles for process models w/ burnettk cullerton
ab24c28d9 updated recently viewed table to be recently run and added run button w/ burnettk cullerton
9f894a8a9 added link to process model tile w/ burnettk cullerton
b7a0743a5 moved delete and edit model and group buttons to icons on show pages w/ burnettk cullerton
21f7fc917 created new users for keycloak and fixed some permissions for core user w/ burnettk cullerton
bd5a55c04 renamed modifyProcessModelPath to modifyProcessIdentifierForPathParam w/ burnettk
55a59b5ed modify process group id before submitting w/ burnettk
58bf7e38d Allow switching between user defined reports (#56)
ec29be773 added recursive option to process model list to recurse or not and fix some ui components
56ae0afe3 fixed task frontend test
5f8a8dd64 the misc group is now 99-Misc
467e9643c allow longer username
83f6185f1 fix tests and add frontend tests
f3b5cb7ca upgrade apscheduler and fix mispelling
cfe7172de added a script to add a user to a group w/ burnettk
976ca7320 task cypress tests are passing w/ burnettk cullerton
aa1a62505 process model cypress tests are passing w/ burnettk cullerton
e36012401 make sure to pass the correct form of process group id when creating a process model w/ burnettk cullerton
97a840d04 process instance cypress tests pass now w/ burnettk cullerton
86fdb302a allow getting all process models, process instances should not save when they are initialized, and fixed some cypress tests w/ burnettk
2b15e66d2 iterating on cypress
1aa72f420 fix cypress tests

git-subtree-dir: spiffworkflow-frontend
git-subtree-split: 55607af9318775fb3524cc5bb4f6a3c6188efe38
This commit is contained in:
burnettk 2022-12-10 23:39:02 -05:00
parent b198341b00
commit e4e0056581
63 changed files with 2727 additions and 918 deletions

2
.gitignore vendored
View File

@ -29,4 +29,4 @@ cypress/screenshots
/test*.json
# Editors
.idea
.idea

View File

@ -19,25 +19,22 @@ describe('process-groups', () => {
cy.url().should('include', `process-groups/${groupId}`);
cy.contains(`Process Group: ${groupDisplayName}`);
cy.contains('Edit process group').click();
cy.getBySel('edit-process-group-button').click();
cy.get('input[name=display_name]').clear().type(newGroupDisplayName);
cy.contains('Submit').click();
cy.contains(`Process Group: ${newGroupDisplayName}`);
cy.contains('Edit process group').click();
cy.get('input[name=display_name]').should(
'have.value',
newGroupDisplayName
);
cy.contains('Delete').click();
cy.getBySel('delete-process-group-button').click();
cy.contains('Are you sure');
cy.getBySel('modal-confirmation-dialog').find('.cds--btn--danger').click();
cy.getBySel('delete-process-group-button-modal-confirmation-dialog')
.find('.cds--btn--danger')
.click();
cy.url().should('include', `process-groups`);
cy.contains(groupId).should('not.exist');
});
it('can paginate items', () => {
cy.basicPaginationTest();
});
// process groups no longer has pagination post-tiles
// it('can paginate items', () => {
// cy.basicPaginationTest();
// });
});

View File

@ -3,9 +3,9 @@ import { DATE_FORMAT, PROCESS_STATUSES } from '../../src/config';
const filterByDate = (fromDate) => {
cy.get('#date-picker-start-from').clear().type(format(fromDate, DATE_FORMAT));
cy.contains('Start date from').click();
cy.contains('Start date to').click();
cy.get('#date-picker-end-from').clear().type(format(fromDate, DATE_FORMAT));
cy.contains('End date from').click();
cy.contains('End date to').click();
cy.getBySel('filter-button').click();
};
@ -53,9 +53,9 @@ const updateBpmnPythonScriptWithMonaco = (
cy.get('.monaco-editor textarea:first')
.click()
.focused() // change subject to currently focused element
// .type('{ctrl}a') // had been doing it this way, but it turns out to be flaky relative to clear()
.clear()
.type(pythonScript, { delay: 30 });
// long delay to ensure cypress isn't competing with monaco auto complete stuff
.type(pythonScript, { delay: 120 });
cy.contains('Close').click();
// wait for a little bit for the xml to get set before saving
@ -119,28 +119,28 @@ describe('process-instances', () => {
cy.runPrimaryBpmnFile();
});
it('can create a new instance and can modify with monaco text editor', () => {
// leave off the ending double quote since manco adds it
const originalPythonScript = 'person = "Kevin';
const newPythonScript = 'person = "Mike';
const bpmnFile = 'process_model_one.bpmn';
// Change bpmn
cy.getBySel('files-accordion').click();
cy.getBySel(`edit-file-${bpmnFile.replace('.', '-')}`).click();
cy.contains(`Process Model File: ${bpmnFile}`);
updateBpmnPythonScriptWithMonaco(newPythonScript);
cy.contains('acceptance-tests-model-1').click();
cy.runPrimaryBpmnFile();
cy.getBySel('files-accordion').click();
cy.getBySel(`edit-file-${bpmnFile.replace('.', '-')}`).click();
cy.contains(`Process Model File: ${bpmnFile}`);
updateBpmnPythonScriptWithMonaco(originalPythonScript);
cy.contains('acceptance-tests-model-1').click();
cy.runPrimaryBpmnFile();
});
// it('can create a new instance and can modify with monaco text editor', () => {
// // leave off the ending double quote since manco adds it
// const originalPythonScript = 'person = "Kevin';
// const newPythonScript = 'person = "Mike';
//
// const bpmnFile = 'process_model_one.bpmn';
//
// // Change bpmn
// cy.getBySel('files-accordion').click();
// cy.getBySel(`edit-file-${bpmnFile.replace('.', '-')}`).click();
// cy.contains(`Process Model File: ${bpmnFile}`);
// updateBpmnPythonScriptWithMonaco(newPythonScript);
// cy.contains('acceptance-tests-model-1').click();
// cy.runPrimaryBpmnFile();
//
// cy.getBySel('files-accordion').click();
// cy.getBySel(`edit-file-${bpmnFile.replace('.', '-')}`).click();
// cy.contains(`Process Model File: ${bpmnFile}`);
// updateBpmnPythonScriptWithMonaco(originalPythonScript);
// cy.contains('acceptance-tests-model-1').click();
// cy.runPrimaryBpmnFile();
// });
it('can paginate items', () => {
// make sure we have some process instances
@ -174,13 +174,12 @@ describe('process-instances', () => {
if (!['all', 'waiting'].includes(processStatus)) {
cy.get(statusSelect).click();
cy.get(statusSelect).contains(processStatus).click();
// close the dropdown again
cy.get(statusSelect).click();
cy.getBySel('filter-button').click();
// FIXME: wait a little bit for the useEffects to be able to fully set processInstanceFilters
cy.wait(1000);
cy.url().should('include', `status=${processStatus}`);
cy.assertAtLeastOneItemInPaginatedResults();
cy.getBySel(`process-instance-status-${processStatus}`).contains(
processStatus
);
cy.getBySel(`process-instance-status-${processStatus}`);
// there should really only be one, but in CI there are sometimes more
cy.get('div[aria-label="Clear all selected items"]:first').click();
}

View File

@ -1,3 +1,5 @@
import { modifyProcessIdentifierForPathParam } from '../../src/helpers';
describe('process-models', () => {
beforeEach(() => {
cy.login();
@ -9,37 +11,48 @@ describe('process-models', () => {
it('can perform crud operations', () => {
const uuid = () => Cypress._.random(0, 1e6);
const id = uuid();
const groupId = 'acceptance-tests-group-one';
const groupId = 'misc/acceptance-tests-group-one';
const groupDisplayName = 'Acceptance Tests Group One';
const modelDisplayName = `Test Model 2 ${id}`;
const newModelDisplayName = `${modelDisplayName} edited`;
const modelId = `test-model-2-${id}`;
const newModelDisplayName = `${modelDisplayName} edited`;
cy.contains('99-Shared Resources').click();
cy.wait(500);
cy.contains(groupDisplayName).click();
cy.createModel(groupId, modelId, modelDisplayName);
cy.url().should('include', `process-models/${groupId}:${modelId}`);
cy.url().should(
'include',
`process-models/${modifyProcessIdentifierForPathParam(
groupId
)}:${modelId}`
);
cy.contains(`Process Model: ${modelDisplayName}`);
cy.contains('Edit process model').click();
cy.getBySel('edit-process-model-button').click();
cy.get('input[name=display_name]').clear().type(newModelDisplayName);
cy.contains('Submit').click();
cy.contains(`Process Model: ${groupId}/${modelId}`);
cy.contains('Submit').click();
cy.get('input[name=display_name]').should(
'have.value',
newModelDisplayName
);
cy.contains(`Process Model: ${newModelDisplayName}`);
cy.contains('Delete').click();
// go back to process model show by clicking on the breadcrumb
cy.contains(modelId).click();
cy.getBySel('delete-process-model-button').click();
cy.contains('Are you sure');
cy.getBySel('modal-confirmation-dialog').find('.cds--btn--danger').click();
cy.url().should('include', `process-groups/${groupId}`);
cy.getBySel('delete-process-model-button-modal-confirmation-dialog')
.find('.cds--btn--danger')
.click();
cy.url().should(
'include',
`process-groups/${modifyProcessIdentifierForPathParam(groupId)}`
);
cy.contains(modelId).should('not.exist');
});
it('can create new bpmn, dmn, and json files', () => {
const uuid = () => Cypress._.random(0, 1e6);
const id = uuid();
const groupId = 'acceptance-tests-group-one';
const directParentGroupId = 'acceptance-tests-group-one';
const groupId = `misc/${directParentGroupId}`;
const groupDisplayName = 'Acceptance Tests Group One';
const modelDisplayName = `Test Model 2 ${id}`;
const modelId = `test-model-2-${id}`;
@ -48,13 +61,19 @@ describe('process-models', () => {
const dmnFileName = `dmn_test_file_${id}`;
const jsonFileName = `json_test_file_${id}`;
cy.contains('99-Shared Resources').click();
cy.wait(500);
cy.contains(groupDisplayName).click();
cy.createModel(groupId, modelId, modelDisplayName);
cy.contains(groupId).click();
cy.contains(modelId).click();
cy.url().should('include', `process-models/${groupId}:${modelId}`);
cy.contains(directParentGroupId).click();
cy.contains(modelDisplayName).click();
cy.url().should(
'include',
`process-models/${modifyProcessIdentifierForPathParam(
groupId
)}:${modelId}`
);
cy.contains(`Process Model: ${modelDisplayName}`);
cy.getBySel('files-accordion').click();
cy.contains(`${bpmnFileName}.bpmn`).should('not.exist');
cy.contains(`${dmnFileName}.dmn`).should('not.exist');
cy.contains(`${jsonFileName}.json`).should('not.exist');
@ -73,7 +92,7 @@ describe('process-models', () => {
cy.contains(`Process Model File: ${bpmnFileName}`);
cy.contains(modelId).click();
cy.contains(`Process Model: ${modelDisplayName}`);
cy.getBySel('files-accordion').click();
// cy.getBySel('files-accordion').click();
cy.contains(`${bpmnFileName}.bpmn`).should('exist');
// add new dmn file
@ -81,13 +100,17 @@ describe('process-models', () => {
cy.contains(/^Process Model File$/);
cy.get('g[data-element-id=decision_1]').click().should('exist');
cy.contains('General').click();
cy.get('#bio-properties-panel-id')
.clear()
.type('decision_acceptance_test_1');
cy.contains('General').click();
cy.contains('Save').click();
cy.get('input[name=file_name]').type(dmnFileName);
cy.contains('Save Changes').click();
cy.contains(`Process Model File: ${dmnFileName}`);
cy.contains(modelId).click();
cy.contains(`Process Model: ${modelDisplayName}`);
cy.getBySel('files-accordion').click();
// cy.getBySel('files-accordion').click();
cy.contains(`${dmnFileName}.dmn`).should('exist');
// add new json file
@ -103,35 +126,47 @@ describe('process-models', () => {
cy.wait(500);
cy.contains(modelId).click();
cy.contains(`Process Model: ${modelDisplayName}`);
cy.getBySel('files-accordion').click();
// cy.getBySel('files-accordion').click();
cy.contains(`${jsonFileName}.json`).should('exist');
cy.contains('Edit process model').click();
cy.contains('Delete').click();
cy.getBySel('delete-process-model-button').click();
cy.contains('Are you sure');
cy.getBySel('modal-confirmation-dialog').find('.cds--btn--danger').click();
cy.url().should('include', `process-groups/${groupId}`);
cy.getBySel('delete-process-model-button-modal-confirmation-dialog')
.find('.cds--btn--danger')
.click();
cy.url().should(
'include',
`process-groups/${modifyProcessIdentifierForPathParam(groupId)}`
);
cy.contains(modelId).should('not.exist');
cy.contains(modelDisplayName).should('not.exist');
});
it('can upload and run a bpmn file', () => {
const uuid = () => Cypress._.random(0, 1e6);
const id = uuid();
const groupId = 'acceptance-tests-group-one';
const directParentGroupId = 'acceptance-tests-group-one';
const groupId = `misc/${directParentGroupId}`;
const groupDisplayName = 'Acceptance Tests Group One';
const modelDisplayName = `Test Model 2 ${id}`;
const modelId = `test-model-2-${id}`;
cy.contains('Add a process group');
cy.contains('99-Shared Resources').click();
cy.wait(500);
cy.contains(groupDisplayName).click();
cy.createModel(groupId, modelId, modelDisplayName);
cy.contains(`${groupId}`).click();
cy.contains(`${directParentGroupId}`).click();
cy.contains('Add a process model');
cy.contains(modelId).click();
cy.url().should('include', `process-models/${groupId}:${modelId}`);
cy.contains(modelDisplayName).click();
cy.url().should(
'include',
`process-models/${modifyProcessIdentifierForPathParam(
groupId
)}:${modelId}`
);
cy.contains(`Process Model: ${modelDisplayName}`);
cy.getBySel('files-accordion').click();
cy.getBySel('upload-file-button').click();
cy.contains('Add file').selectFile(
'cypress/fixtures/test_bpmn_file_upload.bpmn'
@ -142,31 +177,41 @@ describe('process-models', () => {
.click();
cy.runPrimaryBpmnFile();
cy.getBySel('process-instance-list-link').click();
// cy.getBySel('process-instance-list-link').click();
cy.getBySel('process-instance-show-link').click();
cy.getBySel('process-instance-delete').click();
cy.contains('Are you sure');
cy.getBySel('modal-confirmation-dialog').find('.cds--btn--danger').click();
cy.getBySel('process-instance-delete-modal-confirmation-dialog')
.find('.cds--btn--danger')
.click();
// in breadcrumb
cy.contains(modelId).click();
cy.contains('Edit process model').click();
cy.contains('Delete').click();
cy.getBySel('delete-process-model-button').click();
cy.contains('Are you sure');
cy.getBySel('modal-confirmation-dialog').find('.cds--btn--danger').click();
cy.url().should('include', `process-groups/${groupId}`);
cy.getBySel('delete-process-model-button-modal-confirmation-dialog')
.find('.cds--btn--danger')
.click();
cy.url().should(
'include',
`process-groups/${modifyProcessIdentifierForPathParam(groupId)}`
);
cy.contains(modelId).should('not.exist');
cy.contains(modelDisplayName).should('not.exist');
});
it('can paginate items', () => {
cy.contains('Acceptance Tests Group One').click();
cy.basicPaginationTest();
});
// process models no longer has pagination post-tiles
// it.only('can paginate items', () => {
// cy.contains('99-Shared Resources').click();
// cy.wait(500);
// cy.contains('Acceptance Tests Group One').click();
// cy.basicPaginationTest();
// });
it('can allow searching for model', () => {
cy.getBySel('process-model-selection').click().type('model-3');
cy.contains('acceptance-tests-group-one/acceptance-tests-model-3').click();
cy.contains('List').click();
cy.contains('Acceptance Tests Model 3');
});
});

View File

@ -1,18 +1,27 @@
const submitInputIntoFormField = (taskName, fieldKey, fieldValue) => {
cy.contains(`Task: ${taskName}`);
cy.contains(`Task: ${taskName}`, { timeout: 10000 });
cy.get(fieldKey).clear().type(fieldValue);
cy.contains('Submit').click();
};
const checkFormFieldIsReadOnly = (formName, fieldKey) => {
cy.contains(`Task: ${formName}`);
cy.get(fieldKey).invoke('attr', 'readonly').should('exist');
cy.get(fieldKey).invoke('attr', 'disabled').should('exist');
};
const checkTaskHasClass = (taskName, className) => {
cy.get(`g[data-element-id=${taskName}]`).should('have.class', className);
};
const kickOffModelWithForm = (modelId, formName) => {
cy.navigateToProcessModel(
'Acceptance Tests Group One',
'Acceptance Tests Model 2',
'acceptance-tests-model-2'
);
cy.runPrimaryBpmnFile(true);
};
describe('tasks', () => {
beforeEach(() => {
cy.login();
@ -21,7 +30,6 @@ describe('tasks', () => {
cy.logout();
});
// TODO: need to fix the next_task thing to make this pass
it('can complete and navigate a form', () => {
const groupDisplayName = 'Acceptance Tests Group One';
const modelId = `acceptance-tests-model-2`;
@ -30,11 +38,7 @@ describe('tasks', () => {
const activeTaskClassName = 'active-task-highlight';
cy.navigateToProcessModel(groupDisplayName, modelDisplayName, modelId);
// avoid reloading so we can click on the task link that appears on running the process instance
cy.runPrimaryBpmnFile(false);
cy.contains('my task').click();
cy.runPrimaryBpmnFile(true);
submitInputIntoFormField(
'get_user_generated_number_one',
@ -59,7 +63,6 @@ describe('tasks', () => {
'#root_user_generated_number_1'
);
cy.getBySel('form-nav-form3').should('have.text', 'form3 - Current');
cy.getBySel('form-nav-form3').click();
submitInputIntoFormField(
'get_user_generated_number_three',
@ -111,18 +114,12 @@ describe('tasks', () => {
});
it('can paginate items', () => {
cy.navigateToProcessModel(
'Acceptance Tests Group One',
'Acceptance Tests Model 2',
'acceptance-tests-model-2'
);
// make sure we have some tasks
cy.runPrimaryBpmnFile();
cy.runPrimaryBpmnFile();
cy.runPrimaryBpmnFile();
cy.runPrimaryBpmnFile();
cy.runPrimaryBpmnFile();
kickOffModelWithForm();
kickOffModelWithForm();
kickOffModelWithForm();
kickOffModelWithForm();
kickOffModelWithForm();
cy.navigateToHome();
cy.basicPaginationTest();

View File

@ -1,4 +1,5 @@
import { string } from 'prop-types';
import { modifyProcessIdentifierForPathParam } from '../../src/helpers';
// ***********************************************
// This example commands.js shows you how to
@ -31,9 +32,8 @@ Cypress.Commands.add('getBySel', (selector, ...args) => {
});
Cypress.Commands.add('navigateToHome', () => {
cy.get('button[aria-label="Open menu"]').click();
cy.getBySel('header-menu-expand-button').click();
cy.getBySel('side-nav-items').contains('Home').click();
// cy.getBySel('nav-home').click();
});
Cypress.Commands.add('navigateToAdmin', () => {
@ -76,27 +76,39 @@ Cypress.Commands.add('createModel', (groupId, modelId, modelDisplayName) => {
cy.get('input[name=id]').should('have.value', modelId);
cy.contains('Submit').click();
cy.url().should('include', `process-models/${groupId}:${modelId}`);
cy.url().should(
'include',
`process-models/${modifyProcessIdentifierForPathParam(groupId)}:${modelId}`
);
cy.contains(`Process Model: ${modelDisplayName}`);
});
Cypress.Commands.add('runPrimaryBpmnFile', (reload = true) => {
cy.contains('Run').click();
cy.contains(/Process Instance.*kicked off/);
if (reload) {
cy.reload(true);
cy.contains(/Process Instance.*kicked off/).should('not.exist');
Cypress.Commands.add(
'runPrimaryBpmnFile',
(expectAutoRedirectToHumanTask = false) => {
cy.contains('Run').click();
if (expectAutoRedirectToHumanTask) {
// the url changes immediately, so also make sure we get some content from the next page, "Task:", or else when we try to interact with the page, it'll re-render and we'll get an error with cypress.
cy.url().should('include', `/tasks/`);
cy.contains('Task: ');
} else {
cy.contains(/Process Instance.*kicked off/);
cy.reload(true);
cy.contains(/Process Instance.*kicked off/).should('not.exist');
}
}
});
);
Cypress.Commands.add(
'navigateToProcessModel',
(groupDisplayName, modelDisplayName, modelIdentifier) => {
cy.navigateToAdmin();
cy.contains('99-Shared Resources').click();
cy.contains(`Process Group: 99-Shared Resources`, { timeout: 10000 });
cy.contains(groupDisplayName).click();
cy.contains(`Process Group: ${groupDisplayName}`);
// https://stackoverflow.com/q/51254946/6090676
cy.getBySel('process-model-show-link').contains(modelIdentifier).click();
cy.getBySel('process-model-show-link').contains(modelDisplayName).click();
cy.contains(`Process Model: ${modelDisplayName}`);
}
);
@ -120,13 +132,3 @@ Cypress.Commands.add('assertAtLeastOneItemInPaginatedResults', () => {
Cypress.Commands.add('assertNoItemInPaginatedResults', () => {
cy.contains(/\b00 of 0 items/);
});
Cypress.Commands.add('modifyProcessModelPath', (path) => {
path.replace('/', ':');
return path;
});
Cypress.Commands.add('modifyProcessModelPath', (path) => {
path.replace('/', ':');
return path;
});

4
package-lock.json generated
View File

@ -7980,7 +7980,7 @@
},
"node_modules/bpmn-js-spiffworkflow": {
"version": "0.0.8",
"resolved": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#e92f48da7cb4416310af71bb1699caaca87324cd",
"resolved": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#aca23dc56e5d37aa1ed0a3cf11acb55f76a36da7",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.4",
@ -37138,7 +37138,7 @@
}
},
"bpmn-js-spiffworkflow": {
"version": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#e92f48da7cb4416310af71bb1699caaca87324cd",
"version": "git+ssh://git@github.com/sartography/bpmn-js-spiffworkflow.git#aca23dc56e5d37aa1ed0a3cf11acb55f76a36da7",
"from": "bpmn-js-spiffworkflow@sartography/bpmn-js-spiffworkflow#main",
"requires": {
"inherits": "^2.0.4",

View File

@ -41,4 +41,3 @@
-->
</body>
</html>

View File

@ -46,7 +46,7 @@ export default function ButtonWithConfirmation({
<Modal
open={showConfirmationPrompt}
danger
data-qa="modal-confirmation-dialog"
data-qa={`${dataQa}-modal-confirmation-dialog`}
modalHeading={description}
modalLabel={title}
primaryButtonText={confirmButtonLabel}

View File

@ -0,0 +1,22 @@
import { Link } from 'react-router-dom';
import { modifyProcessIdentifierForPathParam } from '../helpers';
import { MessageInstance, ProcessInstance } from '../interfaces';
export function FormatProcessModelDisplayName(
instanceObject: ProcessInstance | MessageInstance
) {
const {
process_model_identifier: processModelIdentifier,
process_model_display_name: processModelDisplayName,
} = instanceObject;
return (
<Link
to={`/admin/process-models/${modifyProcessIdentifierForPathParam(
processModelIdentifier
)}`}
title={processModelIdentifier}
>
{processModelDisplayName}
</Link>
);
}

View File

@ -8,6 +8,8 @@ export default function MyCompletedInstances() {
filtersEnabled={false}
paginationQueryParamPrefix={paginationQueryParamPrefix}
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_instances_initiated_by_me"
showReports={false}
/>
);
}

View File

@ -74,7 +74,9 @@ export default function NavigationBar() {
if (UserService.isLoggedIn()) {
return (
<>
<HeaderGlobalAction>{UserService.getUsername()}</HeaderGlobalAction>
<HeaderGlobalAction className="username-header-text">
{UserService.getUsername()}
</HeaderGlobalAction>
<HeaderGlobalAction
aria-label="Logout"
onClick={handleLogout}
@ -161,6 +163,7 @@ export default function NavigationBar() {
</Can>
{configurationElement()}
<HeaderMenuItem
hidden
href="/admin/process-instances/reports"
isCurrentPage={isActivePage('/admin/process-instances/reports')}
>
@ -177,6 +180,7 @@ export default function NavigationBar() {
<Header aria-label="IBM Platform Name" className="cds--g100">
<SkipToContent />
<HeaderMenuButton
data-qa="header-menu-expand-button"
aria-label="Open menu"
onClick={onClickSideNavExpand}
isActive={isSideNavExpanded}

View File

@ -0,0 +1,48 @@
import React from 'react';
// @ts-ignore
import { Close, CheckmarkFilled } from '@carbon/icons-react';
// @ts-ignore
import { Button } from '@carbon/react';
type OwnProps = {
title: string;
children: React.ReactNode;
onClose: (..._args: any[]) => any;
type?: string;
};
export function Notification({
title,
children,
onClose,
type = 'success',
}: OwnProps) {
let iconClassName = 'green-icon';
if (type === 'error') {
iconClassName = 'red-icon';
}
return (
<div
role="status"
className={`with-bottom-margin cds--inline-notification cds--inline-notification--low-contrast cds--inline-notification--${type}`}
>
<div className="cds--inline-notification__details">
<div className="cds--inline-notification__text-wrapper">
<CheckmarkFilled className={`${iconClassName} notification-icon`} />
<div className="cds--inline-notification__title">{title}</div>
<div className="cds--inline-notification__subtitle">{children}</div>
</div>
</div>
<Button
data-qa="close-publish-notification"
renderIcon={Close}
iconDescription="Close Notification"
className="cds--inline-notification__close-button"
hasIconOnly
size="sm"
kind=""
onClick={onClose}
/>
</div>
);
}

View File

@ -14,6 +14,7 @@ type OwnProps = {
pagination: PaginationObject | null;
tableToDisplay: any;
paginationQueryParamPrefix?: string;
paginationClassName?: string;
};
export default function PaginationForTable({
@ -23,6 +24,7 @@ export default function PaginationForTable({
pagination,
tableToDisplay,
paginationQueryParamPrefix,
paginationClassName,
}: OwnProps) {
const PER_PAGE_OPTIONS = [2, 10, 50, 100];
const [searchParams, setSearchParams] = useSearchParams();
@ -44,6 +46,7 @@ export default function PaginationForTable({
<>
{tableToDisplay}
<Pagination
className={paginationClassName}
data-qa="pagination-options"
backwardText="Previous page"
forwardText="Next page"

View File

@ -3,13 +3,13 @@ import { BrowserRouter } from 'react-router-dom';
import ProcessBreadcrumb from './ProcessBreadcrumb';
test('renders home link', () => {
render(
<BrowserRouter>
<ProcessBreadcrumb />
</BrowserRouter>
);
const homeElement = screen.getByText(/Process Groups/);
expect(homeElement).toBeInTheDocument();
// render(
// <BrowserRouter>
// <ProcessBreadcrumb />
// </BrowserRouter>
// );
// const homeElement = screen.getByText(/Process Groups/);
// expect(homeElement).toBeInTheDocument();
});
test('renders hotCrumbs', () => {

View File

@ -1,123 +1,118 @@
// @ts-ignore
import { Breadcrumb, BreadcrumbItem } from '@carbon/react';
import { splitProcessModelId } from '../helpers';
import { HotCrumbItem } from '../interfaces';
import { useEffect, useState } from 'react';
import { modifyProcessIdentifierForPathParam } from '../helpers';
import {
HotCrumbItem,
ProcessGroup,
ProcessGroupLite,
ProcessModel,
} from '../interfaces';
import HttpService from '../services/HttpService';
type OwnProps = {
processModelId?: string;
processGroupId?: string;
linkProcessModel?: boolean;
hotCrumbs?: HotCrumbItem[];
};
const explodeCrumb = (crumb: HotCrumbItem) => {
const url: string = crumb[1] || '';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [endingUrlType, processModelId, link] = url.split(':');
const processModelIdSegments = splitProcessModelId(processModelId);
const paths: string[] = [];
const lastPathItem = processModelIdSegments.pop();
const breadcrumbItems = processModelIdSegments.map(
(processModelIdSegment: string) => {
paths.push(processModelIdSegment);
const fullUrl = `/admin/process-groups/${paths.join(':')}`;
return (
<BreadcrumbItem key={processModelIdSegment} href={fullUrl}>
{processModelIdSegment}
</BreadcrumbItem>
);
}
);
if (link === 'link') {
if (lastPathItem !== undefined) {
paths.push(lastPathItem);
}
// process_model to process-models
const lastUrl = `/admin/${endingUrlType
.replace('_', '-')
.replace(/s*$/, 's')}/${paths.join(':')}`;
breadcrumbItems.push(
<BreadcrumbItem key={lastPathItem} href={lastUrl}>
{lastPathItem}
</BreadcrumbItem>
);
} else {
breadcrumbItems.push(
<BreadcrumbItem isCurrentPage key={lastPathItem}>
{lastPathItem}
</BreadcrumbItem>
);
}
return breadcrumbItems;
};
export default function ProcessBreadcrumb({ hotCrumbs }: OwnProps) {
const [processEntity, setProcessEntity] = useState<
ProcessGroup | ProcessModel | null
>(null);
export default function ProcessBreadcrumb({
processModelId,
processGroupId,
hotCrumbs,
linkProcessModel = false,
}: OwnProps) {
let processGroupBreadcrumb = null;
let processModelBreadcrumb = null;
if (hotCrumbs) {
const leadingCrumbLinks = hotCrumbs.map((crumb: any) => {
const valueLabel = crumb[0];
const url = crumb[1];
if (!url) {
return (
<BreadcrumbItem isCurrentPage key={valueLabel}>
{valueLabel}
</BreadcrumbItem>
);
useEffect(() => {
const explodeCrumbItemObject = (crumb: HotCrumbItem) => {
if ('entityToExplode' in crumb) {
const { entityToExplode, entityType } = crumb;
if (entityType === 'process-model-id') {
HttpService.makeCallToBackend({
path: `/process-models/${modifyProcessIdentifierForPathParam(
entityToExplode as string
)}`,
successCallback: setProcessEntity,
});
} else if (entityType === 'process-group-id') {
HttpService.makeCallToBackend({
path: `/process-groups/${modifyProcessIdentifierForPathParam(
entityToExplode as string
)}`,
successCallback: setProcessEntity,
});
} else {
setProcessEntity(entityToExplode as any);
}
}
if (url && url.match(/^process[_-](model|group)s?:/)) {
return explodeCrumb(crumb);
}
return (
<BreadcrumbItem key={valueLabel} href={url}>
{valueLabel}
</BreadcrumbItem>
);
});
return <Breadcrumb noTrailingSlash>{leadingCrumbLinks}</Breadcrumb>;
}
if (processModelId) {
if (linkProcessModel) {
processModelBreadcrumb = (
<BreadcrumbItem
href={`/admin/process-models/${processGroupId}/${processModelId}`}
>
{`Process Model: ${processModelId}`}
</BreadcrumbItem>
);
} else {
processModelBreadcrumb = (
<BreadcrumbItem isCurrentPage>
{`Process Model: ${processModelId}`}
</BreadcrumbItem>
);
};
if (hotCrumbs) {
hotCrumbs.forEach(explodeCrumbItemObject);
}
processGroupBreadcrumb = (
<BreadcrumbItem
data-qa="process-group-breadcrumb-link"
href={`/admin/process-groups/${processGroupId}`}
>
{`Process Group: ${processGroupId}`}
</BreadcrumbItem>
);
} else if (processGroupId) {
processGroupBreadcrumb = (
<BreadcrumbItem isCurrentPage>
{`Process Group: ${processGroupId}`}
</BreadcrumbItem>
);
}
}, [setProcessEntity, hotCrumbs]);
return (
<Breadcrumb noTrailingSlash>
<BreadcrumbItem href="/admin">Process Groups</BreadcrumbItem>
{processGroupBreadcrumb}
{processModelBreadcrumb}
</Breadcrumb>
);
// eslint-disable-next-line sonarjs/cognitive-complexity
const hotCrumbElement = () => {
if (hotCrumbs) {
const leadingCrumbLinks = hotCrumbs.map((crumb: any) => {
if (
'entityToExplode' in crumb &&
processEntity &&
processEntity.parent_groups
) {
const breadcrumbs = processEntity.parent_groups.map(
(parentGroup: ProcessGroupLite) => {
const fullUrl = `/admin/process-groups/${modifyProcessIdentifierForPathParam(
parentGroup.id
)}`;
return (
<BreadcrumbItem key={parentGroup.id} href={fullUrl}>
{parentGroup.display_name}
</BreadcrumbItem>
);
}
);
if (crumb.linkLastItem) {
let apiBase = '/admin/process-groups';
if (crumb.entityType.startsWith('process-model')) {
apiBase = '/admin/process-models';
}
const fullUrl = `${apiBase}/${modifyProcessIdentifierForPathParam(
processEntity.id
)}`;
breadcrumbs.push(
<BreadcrumbItem key={processEntity.id} href={fullUrl}>
{processEntity.display_name}
</BreadcrumbItem>
);
} else {
breadcrumbs.push(
<BreadcrumbItem key={processEntity.id} isCurrentPage>
{processEntity.display_name}
</BreadcrumbItem>
);
}
return breadcrumbs;
}
const valueLabel = crumb[0];
const url = crumb[1];
if (!url && valueLabel) {
return (
<BreadcrumbItem isCurrentPage key={valueLabel}>
{valueLabel}
</BreadcrumbItem>
);
}
if (url && valueLabel) {
return (
<BreadcrumbItem key={valueLabel} href={url}>
{valueLabel}
</BreadcrumbItem>
);
}
return null;
});
return <Breadcrumb noTrailingSlash>{leadingCrumbLinks}</Breadcrumb>;
}
return null;
};
return <Breadcrumb noTrailingSlash>{hotCrumbElement()}</Breadcrumb>;
}

View File

@ -2,10 +2,9 @@ import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
// @ts-ignore
import { Button, ButtonSet, Form, Stack, TextInput } from '@carbon/react';
import { modifyProcessModelPath, slugifyString } from '../helpers';
import { modifyProcessIdentifierForPathParam, slugifyString } from '../helpers';
import HttpService from '../services/HttpService';
import { ProcessGroup } from '../interfaces';
import ButtonWithConfirmation from './ButtonWithConfirmation';
type OwnProps = {
mode: string;
@ -28,34 +27,24 @@ export default function ProcessGroupForm({
const navigateToProcessGroup = (_result: any) => {
if (newProcessGroupId) {
navigate(
`/admin/process-groups/${modifyProcessModelPath(newProcessGroupId)}`
`/admin/process-groups/${modifyProcessIdentifierForPathParam(
newProcessGroupId
)}`
);
}
};
const navigateToProcessGroups = (_result: any) => {
navigate(`/admin/process-groups`);
};
const hasValidIdentifier = (identifierToCheck: string) => {
return identifierToCheck.match(/^[a-z0-9][0-9a-z-]+[a-z0-9]$/);
};
const deleteProcessGroup = () => {
HttpService.makeCallToBackend({
path: `/process-groups/${modifyProcessModelPath(processGroup.id)}`,
successCallback: navigateToProcessGroups,
httpMethod: 'DELETE',
});
};
const handleFormSubmission = (event: any) => {
const searchParams = new URLSearchParams(document.location.search);
const parentGroupId = searchParams.get('parentGroupId');
event.preventDefault();
let hasErrors = false;
if (!hasValidIdentifier(processGroup.id)) {
if (mode === 'new' && !hasValidIdentifier(processGroup.id)) {
setIdentifierInvalid(true);
hasErrors = true;
}
@ -68,7 +57,9 @@ export default function ProcessGroupForm({
}
let path = '/process-groups';
if (mode === 'edit') {
path = `/process-groups/${processGroup.id}`;
path = `/process-groups/${modifyProcessIdentifierForPathParam(
processGroup.id
)}`;
}
let httpMethod = 'POST';
if (mode === 'edit') {
@ -124,7 +115,6 @@ export default function ProcessGroupForm({
labelText="Display Name*"
value={processGroup.display_name}
onChange={(event: any) => onDisplayNameChanged(event.target.value)}
onBlur={(event: any) => console.log('event', event)}
/>,
];
@ -166,16 +156,6 @@ export default function ProcessGroupForm({
const formButtons = () => {
const buttons = [<Button type="submit">Submit</Button>];
if (mode === 'edit') {
buttons.push(
<ButtonWithConfirmation
description={`Delete Process Group ${processGroup.id}?`}
onConfirmation={deleteProcessGroup}
buttonLabel="Delete"
confirmButtonLabel="Delete"
/>
);
}
return <ButtonSet>{buttons}</ButtonSet>;
};

View File

@ -10,7 +10,10 @@ import {
} from '@carbon/react';
import HttpService from '../services/HttpService';
import { ProcessGroup } from '../interfaces';
import { modifyProcessModelPath, truncateString } from '../helpers';
import {
modifyProcessIdentifierForPathParam,
truncateString,
} from '../helpers';
type OwnProps = {
processGroup?: ProcessGroup;
@ -51,9 +54,11 @@ export default function ProcessGroupListTiles({
displayText = (processGroups || []).map((row: ProcessGroup) => {
return (
<ClickableTile
id="tile-1"
id={`process-group-tile-${row.id}`}
className="tile-process-group"
href={`/admin/process-groups/${modifyProcessModelPath(row.id)}`}
href={`/admin/process-groups/${modifyProcessIdentifierForPathParam(
row.id
)}`}
>
<div className="tile-process-group-content-container">
<ArrowRight />
@ -61,7 +66,7 @@ export default function ProcessGroupListTiles({
{row.display_name}
</div>
<p className="tile-description">
{truncateString(row.description || '', 25)}
{truncateString(row.description || '', 100)}
</p>
<p className="tile-process-group-children-count tile-pin-bottom">
Total Sub Items: {processGroupDirectChildrenCount(row)}

View File

@ -0,0 +1,205 @@
import { useState } from 'react';
import {
Button,
TextInput,
Stack,
Modal,
// @ts-ignore
} from '@carbon/react';
import {
ReportFilter,
ProcessInstanceReport,
ProcessModel,
ReportColumn,
ReportMetadata,
} from '../interfaces';
import HttpService from '../services/HttpService';
type OwnProps = {
onSuccess: (..._args: any[]) => any;
columnArray: ReportColumn[];
orderBy: string;
processModelSelection: ProcessModel | null;
processStatusSelection: string[];
startFromSeconds: string | null;
startToSeconds: string | null;
endFromSeconds: string | null;
endToSeconds: string | null;
buttonText?: string;
buttonClassName?: string;
processInstanceReportSelection?: ProcessInstanceReport | null;
reportMetadata: ReportMetadata;
};
export default function ProcessInstanceListSaveAsReport({
onSuccess,
columnArray,
orderBy,
processModelSelection,
processInstanceReportSelection,
processStatusSelection,
startFromSeconds,
startToSeconds,
endFromSeconds,
endToSeconds,
buttonClassName,
buttonText = 'Save as Perspective',
reportMetadata,
}: OwnProps) {
const [identifier, setIdentifier] = useState<string>(
processInstanceReportSelection?.identifier || ''
);
const [showSaveForm, setShowSaveForm] = useState<boolean>(false);
const isEditMode = () => {
return (
processInstanceReportSelection &&
processInstanceReportSelection.identifier === identifier
);
};
const responseHandler = (result: any) => {
if (result) {
onSuccess(result, isEditMode() ? 'edit' : 'new');
}
};
const handleSaveFormClose = () => {
setIdentifier(processInstanceReportSelection?.identifier || '');
setShowSaveForm(false);
};
const addProcessInstanceReport = (event: any) => {
event.preventDefault();
// TODO: make a field to set this
let orderByArray = ['-start_in_seconds', '-id'];
if (orderBy) {
orderByArray = orderBy.split(',').filter((n) => n);
}
const filterByArray: any = [];
if (processModelSelection) {
filterByArray.push({
field_name: 'process_model_identifier',
field_value: processModelSelection.id,
});
}
if (processStatusSelection.length > 0) {
filterByArray.push({
field_name: 'process_status',
field_value: processStatusSelection.join(','),
operator: 'in',
});
}
if (startFromSeconds) {
filterByArray.push({
field_name: 'start_from',
field_value: startFromSeconds,
});
}
if (startToSeconds) {
filterByArray.push({
field_name: 'start_to',
field_value: startToSeconds,
});
}
if (endFromSeconds) {
filterByArray.push({
field_name: 'end_from',
field_value: endFromSeconds,
});
}
if (endToSeconds) {
filterByArray.push({
field_name: 'end_to',
field_value: endToSeconds,
});
}
reportMetadata.filter_by.forEach((reportFilter: ReportFilter) => {
columnArray.forEach((reportColumn: ReportColumn) => {
if (
reportColumn.accessor === reportFilter.field_name &&
reportColumn.filterable
) {
filterByArray.push(reportFilter);
}
});
});
let path = `/process-instances/reports`;
let httpMethod = 'POST';
if (isEditMode() && processInstanceReportSelection) {
httpMethod = 'PUT';
path = `${path}/${processInstanceReportSelection.id}`;
}
HttpService.makeCallToBackend({
path,
successCallback: responseHandler,
httpMethod,
postBody: {
identifier,
report_metadata: {
columns: columnArray,
order_by: orderByArray,
filter_by: filterByArray,
},
},
});
handleSaveFormClose();
};
let textInputComponent = null;
textInputComponent = (
<TextInput
id="identifier"
name="identifier"
labelText="Identifier"
className="no-wrap"
inline
value={identifier}
onChange={(e: any) => setIdentifier(e.target.value)}
/>
);
let descriptionText =
'Save the current columns and filters as a perspective so you can come back to this view in the future.';
if (processInstanceReportSelection) {
descriptionText =
'Keep the identifier the same and click Save to update the current perspective. Change the identifier if you want to save the current view with a new name.';
}
return (
<Stack gap={5} orientation="horizontal">
<Modal
open={showSaveForm}
modalHeading="Save Perspective"
primaryButtonText="Save"
primaryButtonDisabled={!identifier}
onRequestSubmit={addProcessInstanceReport}
onRequestClose={handleSaveFormClose}
hasScrollingContent
>
<p className="data-table-description">{descriptionText}</p>
{textInputComponent}
</Modal>
<Button
kind=""
className={buttonClassName}
onClick={() => {
setIdentifier(processInstanceReportSelection?.identifier || '');
setShowSaveForm(true);
}}
>
{buttonText}
</Button>
</Stack>
);
}

View File

@ -7,7 +7,7 @@ import {
} from 'react-router-dom';
// @ts-ignore
import { Filter } from '@carbon/icons-react';
import { Filter, Close, AddAlt } from '@carbon/icons-react';
import {
Button,
ButtonSet,
@ -21,6 +21,12 @@ import {
TableHead,
TableRow,
TimePicker,
Tag,
Stack,
Modal,
ComboBox,
TextInput,
FormLabel,
// @ts-ignore
} from '@carbon/react';
import { PROCESS_STATUSES, DATE_FORMAT, DATE_FORMAT_CARBON } from '../config';
@ -32,7 +38,8 @@ import {
convertSecondsToFormattedTimeHoursMinutes,
getPageInfoFromSearchParams,
getProcessModelFullIdentifierFromSearchParams,
modifyProcessModelPath,
modifyProcessIdentifierForPathParam,
refreshAtInterval,
} from '../helpers';
import PaginationForTable from './PaginationForTable';
@ -43,14 +50,35 @@ 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 {
PaginationObject,
ProcessModel,
ProcessInstanceReport,
ProcessInstance,
ReportColumn,
ReportColumnForEditing,
ReportMetadata,
ReportFilter,
} from '../interfaces';
import ProcessModelSearch from './ProcessModelSearch';
import ProcessInstanceReportSearch from './ProcessInstanceReportSearch';
import ProcessInstanceListSaveAsReport from './ProcessInstanceListSaveAsReport';
import { FormatProcessModelDisplayName } from './MiniComponents';
import { Notification } from './Notification';
const REFRESH_INTERVAL = 5;
const REFRESH_TIMEOUT = 600;
type OwnProps = {
filtersEnabled?: boolean;
processModelFullIdentifier?: string;
paginationQueryParamPrefix?: string;
perPageOptions?: number[];
showReports?: boolean;
reportIdentifier?: string;
textToShowIfEmpty?: string;
paginationClassName?: string;
autoReload?: boolean;
};
interface dateParameters {
@ -62,13 +90,18 @@ export default function ProcessInstanceListTable({
processModelFullIdentifier,
paginationQueryParamPrefix,
perPageOptions,
showReports = true,
reportIdentifier,
textToShowIfEmpty,
paginationClassName,
autoReload = false,
}: OwnProps) {
const params = useParams();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [processInstances, setProcessInstances] = useState([]);
const [reportMetadata, setReportMetadata] = useState({});
const [reportMetadata, setReportMetadata] = useState<ReportMetadata | null>();
const [pagination, setPagination] = useState<PaginationObject | null>(null);
const [processInstanceFilters, setProcessInstanceFilters] = useState({});
@ -102,6 +135,19 @@ export default function ProcessInstanceListTable({
>([]);
const [processModelSelection, setProcessModelSelection] =
useState<ProcessModel | null>(null);
const [processInstanceReportSelection, setProcessInstanceReportSelection] =
useState<ProcessInstanceReport | null>(null);
const [availableReportColumns, setAvailableReportColumns] = useState<
ReportColumn[]
>([]);
const [processInstanceReportJustSaved, setProcessInstanceReportJustSaved] =
useState<string | null>(null);
const [showReportColumnForm, setShowReportColumnForm] =
useState<boolean>(false);
const [reportColumnToOperateOn, setReportColumnToOperateOn] =
useState<ReportColumnForEditing | null>(null);
const [reportColumnFormMode, setReportColumnFormMode] = useState<string>('');
const dateParametersToAlwaysFilterBy: dateParameters = useMemo(() => {
return {
@ -133,9 +179,13 @@ export default function ProcessInstanceListTable({
function setProcessInstancesFromResult(result: any) {
const processInstancesFromApi = result.results;
setProcessInstances(processInstancesFromApi);
setReportMetadata(result.report_metadata);
setPagination(result.pagination);
setProcessInstanceFilters(result.filters);
setReportMetadata(result.report.report_metadata);
if (result.report.id) {
setProcessInstanceReportSelection(result.report);
}
}
function getProcessInstances() {
// eslint-disable-next-line prefer-const
@ -156,6 +206,12 @@ export default function ProcessInstanceListTable({
queryParamString += `&user_filter=${userAppliedFilter}`;
}
if (searchParams.get('report_id')) {
queryParamString += `&report_id=${searchParams.get('report_id')}`;
} else if (reportIdentifier) {
queryParamString += `&report_identifier=${reportIdentifier}`;
}
Object.keys(dateParametersToAlwaysFilterBy).forEach(
(paramName: string) => {
const dateFunctionToCall =
@ -230,17 +286,24 @@ export default function ProcessInstanceListTable({
getProcessInstances();
}
const checkFiltersAndRun = () => {
if (filtersEnabled) {
// populate process model selection
HttpService.makeCallToBackend({
path: `/process-models?per_page=1000&recursive=true`,
successCallback: processResultForProcessModels,
});
} else {
getProcessInstances();
}
};
if (filtersEnabled) {
// populate process model selection
HttpService.makeCallToBackend({
path: `/process-models?per_page=1000`,
successCallback: processResultForProcessModels,
});
} else {
getProcessInstances();
checkFiltersAndRun();
if (autoReload) {
refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, checkFiltersAndRun);
}
}, [
autoReload,
searchParams,
params,
oneMonthInSeconds,
@ -251,6 +314,7 @@ export default function ProcessInstanceListTable({
paginationQueryParamPrefix,
processModelFullIdentifier,
perPageOptions,
reportIdentifier,
]);
// This sets the filter data using the saved reports returned from the initial instance_list query.
@ -301,6 +365,28 @@ export default function ProcessInstanceListTable({
processModelAvailableItems,
]);
const processInstanceReportSaveTag = () => {
if (processInstanceReportJustSaved) {
let titleOperation = 'Updated';
if (processInstanceReportJustSaved === 'new') {
titleOperation = 'Created';
}
return (
<Notification
title={`Perspective: ${titleOperation}`}
onClose={() => setProcessInstanceReportJustSaved(null)}
>
<span>{`'${
processInstanceReportSelection
? processInstanceReportSelection.identifier
: ''
}'`}</span>
</Notification>
);
}
return null;
};
// 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) => {
@ -318,16 +404,8 @@ export default function ProcessInstanceListTable({
}
};
const applyFilter = (event: any) => {
event.preventDefault();
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
undefined,
undefined,
paginationQueryParamPrefix
);
let queryParamString = `per_page=${perPage}&page=${page}&user_filter=true`;
// TODO: after factoring this out page hangs when invalid date ranges and applying the filter
const calculateStartAndEndSeconds = () => {
const startFromSeconds = convertDateAndTimeStringsToSeconds(
startFromDate,
startFromTime || '00:00:00'
@ -344,28 +422,59 @@ export default function ProcessInstanceListTable({
endToDate,
endToTime || '00:00:00'
);
let valid = true;
if (isTrueComparison(startFromSeconds, '>', startToSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "start date to"',
});
return;
valid = false;
}
if (isTrueComparison(endFromSeconds, '>', endToSeconds)) {
setErrorMessage({
message: '"End date from" cannot be after "end date to"',
});
return;
valid = false;
}
if (isTrueComparison(startFromSeconds, '>', endFromSeconds)) {
setErrorMessage({
message: '"Start date from" cannot be after "end date from"',
});
return;
valid = false;
}
if (isTrueComparison(startToSeconds, '>', endToSeconds)) {
setErrorMessage({
message: '"Start date to" cannot be after "end date to"',
});
valid = false;
}
return {
valid,
startFromSeconds,
startToSeconds,
endFromSeconds,
endToSeconds,
};
};
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 {
valid,
startFromSeconds,
startToSeconds,
endFromSeconds,
endToSeconds,
} = calculateStartAndEndSeconds();
if (!valid) {
return;
}
@ -389,7 +498,12 @@ export default function ProcessInstanceListTable({
queryParamString += `&process_model_identifier=${processModelSelection.id}`;
}
if (processInstanceReportSelection) {
queryParamString += `&report_id=${processInstanceReportSelection.id}`;
}
setErrorMessage(null);
setProcessInstanceReportJustSaved(null);
navigate(`/admin/process-instances?${queryParamString}`);
};
@ -477,12 +591,369 @@ export default function ProcessInstanceListTable({
setEndToTime('');
};
const processInstanceReportDidChange = (selection: any, mode?: string) => {
clearFilters();
const selectedReport = selection.selectedItem;
setProcessInstanceReportSelection(selectedReport);
let queryParamString = '';
if (selectedReport) {
queryParamString = `?report_id=${selectedReport.id}`;
}
setErrorMessage(null);
setProcessInstanceReportJustSaved(mode || null);
navigate(`/admin/process-instances${queryParamString}`);
};
const reportColumns = () => {
return (reportMetadata as any).columns;
};
const reportColumnAccessors = () => {
return reportColumns().map((reportColumn: ReportColumn) => {
return reportColumn.accessor;
});
};
// TODO onSuccess reload/select the new report in the report search
const onSaveReportSuccess = (result: any, mode: string) => {
processInstanceReportDidChange(
{
selectedItem: result,
},
mode
);
};
const saveAsReportComponent = () => {
const {
valid,
startFromSeconds,
startToSeconds,
endFromSeconds,
endToSeconds,
} = calculateStartAndEndSeconds();
if (!valid || !reportMetadata) {
return null;
}
return (
<ProcessInstanceListSaveAsReport
onSuccess={onSaveReportSuccess}
buttonClassName="button-white-background narrow-button"
columnArray={reportColumns()}
orderBy=""
buttonText="Save"
processModelSelection={processModelSelection}
processStatusSelection={processStatusSelection}
processInstanceReportSelection={processInstanceReportSelection}
reportMetadata={reportMetadata}
startFromSeconds={startFromSeconds}
startToSeconds={startToSeconds}
endFromSeconds={endFromSeconds}
endToSeconds={endToSeconds}
/>
);
};
const removeColumn = (reportColumn: ReportColumn) => {
if (reportMetadata) {
const reportMetadataCopy = { ...reportMetadata };
const newColumns = reportColumns().filter(
(rc: ReportColumn) => rc.accessor !== reportColumn.accessor
);
Object.assign(reportMetadataCopy, { columns: newColumns });
setReportMetadata(reportMetadataCopy);
}
};
const handleColumnFormClose = () => {
setShowReportColumnForm(false);
setReportColumnFormMode('');
setReportColumnToOperateOn(null);
};
const getFilterByFromReportMetadata = (reportColumnAccessor: string) => {
if (reportMetadata) {
return reportMetadata.filter_by.find((reportFilter: ReportFilter) => {
return reportColumnAccessor === reportFilter.field_name;
});
}
return null;
};
const getNewFiltersFromReportForEditing = (
reportColumnForEditing: ReportColumnForEditing
) => {
if (!reportMetadata) {
return null;
}
const reportMetadataCopy = { ...reportMetadata };
let newReportFilters = reportMetadataCopy.filter_by;
if (reportColumnForEditing.filterable) {
const newReportFilter: ReportFilter = {
field_name: reportColumnForEditing.accessor,
field_value: reportColumnForEditing.filter_field_value,
operator: reportColumnForEditing.filter_operator || 'equals',
};
const existingReportFilter = getFilterByFromReportMetadata(
reportColumnForEditing.accessor
);
if (existingReportFilter) {
const existingReportFilterIndex =
reportMetadataCopy.filter_by.indexOf(existingReportFilter);
if (reportColumnForEditing.filter_field_value) {
newReportFilters[existingReportFilterIndex] = newReportFilter;
} else {
newReportFilters.splice(existingReportFilterIndex, 1);
}
} else if (reportColumnForEditing.filter_field_value) {
newReportFilters = newReportFilters.concat([newReportFilter]);
}
}
return newReportFilters;
};
const handleUpdateReportColumn = () => {
if (reportColumnToOperateOn && reportMetadata) {
const reportMetadataCopy = { ...reportMetadata };
let newReportColumns = null;
if (reportColumnFormMode === 'new') {
newReportColumns = reportColumns().concat([reportColumnToOperateOn]);
} else {
newReportColumns = reportColumns().map((rc: ReportColumn) => {
if (rc.accessor === reportColumnToOperateOn.accessor) {
return reportColumnToOperateOn;
}
return rc;
});
}
Object.assign(reportMetadataCopy, {
columns: newReportColumns,
filter_by: getNewFiltersFromReportForEditing(reportColumnToOperateOn),
});
setReportMetadata(reportMetadataCopy);
setReportColumnToOperateOn(null);
setShowReportColumnForm(false);
setShowReportColumnForm(false);
}
};
const reportColumnToReportColumnForEditing = (reportColumn: ReportColumn) => {
const reportColumnForEditing: ReportColumnForEditing = Object.assign(
reportColumn,
{ filter_field_value: '', filter_operator: '' }
);
const reportFilter = getFilterByFromReportMetadata(
reportColumnForEditing.accessor
);
if (reportFilter) {
reportColumnForEditing.filter_field_value = reportFilter.field_value;
reportColumnForEditing.filter_operator =
reportFilter.operator || 'equals';
}
return reportColumnForEditing;
};
const updateReportColumn = (event: any) => {
const reportColumnForEditing = reportColumnToReportColumnForEditing(
event.selectedItem
);
setReportColumnToOperateOn(reportColumnForEditing);
};
// options includes item and inputValue
const shouldFilterReportColumn = (options: any) => {
const reportColumn: ReportColumn = options.item;
const { inputValue } = options;
return (
!reportColumnAccessors().includes(reportColumn.accessor) &&
(reportColumn.accessor || '')
.toLowerCase()
.includes((inputValue || '').toLowerCase())
);
};
const setReportColumnConditionValue = (event: any) => {
if (reportColumnToOperateOn) {
const reportColumnToOperateOnCopy = {
...reportColumnToOperateOn,
};
reportColumnToOperateOnCopy.filter_field_value = event.target.value;
setReportColumnToOperateOn(reportColumnToOperateOnCopy);
}
};
const reportColumnForm = () => {
if (reportColumnFormMode === '') {
return null;
}
const formElements = [
<TextInput
id="report-column-display-name"
name="report-column-display-name"
labelText="Display Name"
disabled={!reportColumnToOperateOn}
value={reportColumnToOperateOn ? reportColumnToOperateOn.Header : ''}
onChange={(event: any) => {
if (reportColumnToOperateOn) {
const reportColumnToOperateOnCopy = {
...reportColumnToOperateOn,
};
reportColumnToOperateOnCopy.Header = event.target.value;
setReportColumnToOperateOn(reportColumnToOperateOnCopy);
}
}}
/>,
];
if (reportColumnToOperateOn && reportColumnToOperateOn.filterable) {
formElements.push(
<TextInput
id="report-column-condition-value"
name="report-column-condition-value"
labelText="Condition Value"
value={
reportColumnToOperateOn
? reportColumnToOperateOn.filter_field_value
: ''
}
onChange={setReportColumnConditionValue}
/>
);
}
if (reportColumnFormMode === 'new') {
formElements.push(
<ComboBox
onChange={updateReportColumn}
className="combo-box-in-modal"
id="report-column-selection"
data-qa="report-column-selection"
data-modal-primary-focus
items={availableReportColumns}
itemToString={(reportColumn: ReportColumn) => {
if (reportColumn) {
return reportColumn.accessor;
}
return null;
}}
shouldFilterItem={shouldFilterReportColumn}
placeholder="Choose a report column"
titleText="Report Column"
/>
);
}
const modalHeading =
reportColumnFormMode === 'new'
? 'Add Column'
: `Edit ${
reportColumnToOperateOn ? reportColumnToOperateOn.accessor : ''
} column`;
return (
<Modal
open={showReportColumnForm}
modalHeading={modalHeading}
primaryButtonText="Save"
primaryButtonDisabled={!reportColumnToOperateOn}
onRequestSubmit={handleUpdateReportColumn}
onRequestClose={handleColumnFormClose}
hasScrollingContent
>
{formElements}
</Modal>
);
};
const columnSelections = () => {
if (reportColumns()) {
const tags: any = [];
(reportColumns() as any).forEach((reportColumn: ReportColumn) => {
const reportColumnForEditing =
reportColumnToReportColumnForEditing(reportColumn);
let tagType = 'cool-gray';
let tagTypeClass = '';
if (reportColumnForEditing.filterable) {
tagType = 'green';
tagTypeClass = 'tag-type-green';
}
let reportColumnLabel = reportColumnForEditing.Header;
if (reportColumnForEditing.filter_field_value) {
reportColumnLabel = `${reportColumnLabel}=${reportColumnForEditing.filter_field_value}`;
}
tags.push(
<Tag type={tagType} size="sm">
<Button
kind="ghost"
size="sm"
className={`button-tag-icon ${tagTypeClass}`}
title={`Edit ${reportColumnForEditing.accessor}`}
onClick={() => {
setReportColumnToOperateOn(reportColumnForEditing);
setShowReportColumnForm(true);
setReportColumnFormMode('edit');
}}
>
{reportColumnLabel}
</Button>
<Button
data-qa="remove-report-column"
renderIcon={Close}
iconDescription="Remove Column"
className={`button-tag-icon ${tagTypeClass}`}
hasIconOnly
size="sm"
kind="ghost"
onClick={() => removeColumn(reportColumnForEditing)}
/>
</Tag>
);
});
return (
<Stack orientation="horizontal">
{tags}
<Button
data-qa="add-column-button"
renderIcon={AddAlt}
iconDescription="Filter Options"
className="with-tiny-top-margin"
kind="ghost"
hasIconOnly
size="sm"
onClick={() => {
setShowReportColumnForm(true);
setReportColumnFormMode('new');
}}
/>
</Stack>
);
}
return null;
};
const filterOptions = () => {
if (!showFilterOptions) {
return null;
}
// get the columns anytime we display the filter options if they are empty
if (availableReportColumns.length < 1) {
HttpService.makeCallToBackend({
path: `/process-instances/reports/columns`,
successCallback: setAvailableReportColumns,
});
}
return (
<>
<Grid fullWidth className="with-bottom-margin">
<Column md={8} lg={16} sm={4}>
<FormLabel>Columns</FormLabel>
<br />
{columnSelections()}
</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={8}>
<ProcessModelSearch
@ -546,11 +1017,11 @@ export default function ProcessInstanceListTable({
</Column>
</Grid>
<Grid fullWidth className="with-bottom-margin">
<Column md={4}>
<Column sm={4} md={4} lg={8}>
<ButtonSet>
<Button
kind=""
className="button-white-background"
className="button-white-background narrow-button"
onClick={clearFilters}
>
Clear
@ -559,11 +1030,15 @@ export default function ProcessInstanceListTable({
kind="secondary"
onClick={applyFilter}
data-qa="filter-button"
className="narrow-button"
>
Filter
</Button>
</ButtonSet>
</Column>
<Column sm={4} md={4} lg={8}>
{saveAsReportComponent()}
</Column>
</Grid>
</>
);
@ -572,28 +1047,30 @@ export default function ProcessInstanceListTable({
const buildTable = () => {
const headerLabels: Record<string, string> = {
id: 'Id',
process_model_identifier: 'Process Model',
process_model_identifier: 'Process',
process_model_display_name: 'Process',
start_in_seconds: 'Start Time',
end_in_seconds: 'End Time',
status: 'Status',
username: 'Started By',
spiff_step: 'SpiffWorkflow Step',
};
const getHeaderLabel = (header: string) => {
return headerLabels[header] ?? header;
};
const headers = (reportMetadata as any).columns.map((column: any) => {
const headers = reportColumns().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
);
const formatProcessInstanceId = (row: ProcessInstance, id: number) => {
const modifiedProcessModelId: String =
modifyProcessIdentifierForPathParam(row.process_model_identifier);
return (
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${row.id}`}
to={`/admin/process-instances/${modifiedProcessModelId}/${id}`}
title={`View process instance ${id}`}
>
{id}
</Link>
@ -602,12 +1079,15 @@ export default function ProcessInstanceListTable({
const formatProcessModelIdentifier = (_row: any, identifier: any) => {
return (
<Link
to={`/admin/process-models/${modifyProcessModelPath(identifier)}`}
to={`/admin/process-models/${modifyProcessIdentifierForPathParam(
identifier
)}`}
>
{identifier}
</Link>
);
};
const formatSecondsForDisplay = (_row: any, seconds: any) => {
return convertSecondsToFormattedDateTime(seconds) || '-';
};
@ -615,14 +1095,16 @@ export default function ProcessInstanceListTable({
return value;
};
const columnFormatters: Record<string, any> = {
const reportColumnFormatters: Record<string, any> = {
id: formatProcessInstanceId,
process_model_identifier: formatProcessModelIdentifier,
process_model_display_name: FormatProcessModelDisplayName,
start_in_seconds: formatSecondsForDisplay,
end_in_seconds: formatSecondsForDisplay,
};
const formattedColumn = (row: any, column: any) => {
const formatter = columnFormatters[column.accessor] ?? defaultFormatter;
const formatter =
reportColumnFormatters[column.accessor] ?? defaultFormatter;
const value = row[column.accessor];
if (column.accessor === 'status') {
return (
@ -635,7 +1117,7 @@ export default function ProcessInstanceListTable({
};
const rows = processInstances.map((row: any) => {
const currentRow = (reportMetadata as any).columns.map((column: any) => {
const currentRow = reportColumns().map((column: any) => {
return formattedColumn(row, column);
});
return <tr key={row.id}>{currentRow}</tr>;
@ -664,6 +1146,25 @@ export default function ProcessInstanceListTable({
setShowFilterOptions(!showFilterOptions);
};
const reportSearchComponent = () => {
if (showReports) {
const columns = [
<Column sm={2} md={4} lg={7}>
<ProcessInstanceReportSearch
onChange={processInstanceReportDidChange}
selectedItem={processInstanceReportSelection}
/>
</Column>,
];
return (
<Grid className="with-tiny-bottom-margin" fullWidth>
{columns}
</Grid>
);
}
return null;
};
const filterComponent = () => {
if (!filtersEnabled) {
return null;
@ -671,14 +1172,17 @@ export default function ProcessInstanceListTable({
return (
<>
<Grid fullWidth>
<Column sm={2} md={4} lg={7}>
{reportSearchComponent()}
</Column>
<Column
className="filterIcon"
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
@ -692,7 +1196,7 @@ export default function ProcessInstanceListTable({
);
};
if (pagination) {
if (pagination && (!textToShowIfEmpty || pagination.total > 0)) {
// eslint-disable-next-line prefer-const
let { page, perPage } = getPageInfoFromSearchParams(
searchParams,
@ -706,6 +1210,8 @@ export default function ProcessInstanceListTable({
}
return (
<>
{reportColumnForm()}
{processInstanceReportSaveTag()}
{filterComponent()}
<PaginationForTable
page={page}
@ -714,10 +1220,18 @@ export default function ProcessInstanceListTable({
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix}
perPageOptions={perPageOptions}
paginationClassName={paginationClassName}
/>
</>
);
}
if (textToShowIfEmpty) {
return (
<p className="no-results-message with-large-bottom-margin">
{textToShowIfEmpty}
</p>
);
}
return null;
}

View File

@ -0,0 +1,85 @@
import { useEffect, useState } from 'react';
import {
ComboBox,
Stack,
FormLabel,
// @ts-ignore
} from '@carbon/react';
import { useSearchParams } from 'react-router-dom';
import { truncateString } from '../helpers';
import { ProcessInstanceReport } from '../interfaces';
import HttpService from '../services/HttpService';
type OwnProps = {
onChange: (..._args: any[]) => any;
selectedItem?: ProcessInstanceReport | null;
titleText?: string;
};
export default function ProcessInstanceReportSearch({
selectedItem,
onChange,
titleText = 'Process instance perspectives',
}: OwnProps) {
const [processInstanceReports, setProcessInstanceReports] = useState<
ProcessInstanceReport[] | null
>(null);
const [searchParams] = useSearchParams();
const reportId = searchParams.get('report_id');
useEffect(() => {
function setProcessInstanceReportsFromResult(
result: ProcessInstanceReport[]
) {
setProcessInstanceReports(result);
}
HttpService.makeCallToBackend({
path: `/process-instances/reports`,
successCallback: setProcessInstanceReportsFromResult,
});
}, [reportId]);
const reportSelectionString = (
processInstanceReport: ProcessInstanceReport
) => {
return `${truncateString(processInstanceReport.identifier, 20)} (Id: ${
processInstanceReport.id
})`;
};
const shouldFilterProcessInstanceReport = (options: any) => {
const processInstanceReport: ProcessInstanceReport = options.item;
const { inputValue } = options;
return reportSelectionString(processInstanceReport).includes(inputValue);
};
const reportsAvailable = () => {
return processInstanceReports && processInstanceReports.length > 0;
};
if (reportsAvailable()) {
return (
<Stack orientation="horizontal" gap={2}>
<FormLabel className="with-top-margin">{titleText}</FormLabel>
<ComboBox
onChange={onChange}
id="process-instance-report-select"
data-qa="process-instance-report-selection"
items={processInstanceReports}
itemToString={(processInstanceReport: ProcessInstanceReport) => {
if (processInstanceReport) {
return reportSelectionString(processInstanceReport);
}
return null;
}}
shouldFilterItem={shouldFilterProcessInstanceReport}
placeholder="Choose a process instance perspective"
selectedItem={selectedItem}
/>
</Stack>
);
}
return null;
}

View File

@ -4,25 +4,95 @@ import {
Button,
// @ts-ignore
} from '@carbon/react';
import { ProcessModel } from '../interfaces';
import { Can } from '@casl/react';
import {
PermissionsToCheck,
ProcessModel,
RecentProcessModel,
} from '../interfaces';
import HttpService from '../services/HttpService';
import ErrorContext from '../contexts/ErrorContext';
import { modifyProcessModelPath } from '../helpers';
import { modifyProcessIdentifierForPathParam } from '../helpers';
import { usePermissionFetcher } from '../hooks/PermissionService';
const storeRecentProcessModelInLocalStorage = (
processModelForStorage: ProcessModel
) => {
// All values stored in localStorage are strings.
// Grab our recentProcessModels string from localStorage.
const stringFromLocalStorage = window.localStorage.getItem(
'recentProcessModels'
);
// adapted from https://stackoverflow.com/a/59424458/6090676
// If that value is null (meaning that we've never saved anything to that spot in localStorage before), use an empty array as our array. Otherwise, use the value we parse out.
let array: RecentProcessModel[] = [];
if (stringFromLocalStorage !== null) {
// Then parse that string into an actual value.
array = JSON.parse(stringFromLocalStorage);
}
// Here's the value we want to add
const value = {
processModelIdentifier: processModelForStorage.id,
processModelDisplayName: processModelForStorage.display_name,
};
// anything with a processGroupIdentifier is old and busted. leave it behind.
array = array.filter((item) => item.processGroupIdentifier === undefined);
// If our parsed/empty array doesn't already have this value in it...
const matchingItem = array.find(
(item) => item.processModelIdentifier === value.processModelIdentifier
);
if (matchingItem === undefined) {
// add the value to the beginning of the array
array.unshift(value);
// Keep the array to 3 items
if (array.length > 3) {
array.pop();
}
}
// once the old and busted serializations are gone, we can put these two statements inside the above if statement
// turn the array WITH THE NEW VALUE IN IT into a string to prepare it to be stored in localStorage
const stringRepresentingArray = JSON.stringify(array);
// and store it in localStorage as "recentProcessModels"
window.localStorage.setItem('recentProcessModels', stringRepresentingArray);
};
type OwnProps = {
processModel: ProcessModel;
onSuccessCallback: Function;
className?: string;
checkPermissions?: boolean;
};
export default function ProcessInstanceRun({
processModel,
onSuccessCallback,
className,
checkPermissions = true,
}: OwnProps) {
const navigate = useNavigate();
const setErrorMessage = (useContext as any)(ErrorContext)[1];
const modifiedProcessModelId = modifyProcessModelPath(processModel.id);
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
processModel.id
);
const processInstanceCreatePath = `/v1.0/process-instances/${modifiedProcessModelId}`;
let permissionRequestData: PermissionsToCheck = {
[processInstanceCreatePath]: ['POST'],
};
if (!checkPermissions) {
permissionRequestData = {};
}
const { ability } = usePermissionFetcher(permissionRequestData);
const onProcessInstanceRun = (processInstance: any) => {
// FIXME: ensure that the task is actually for the current user as well
@ -36,8 +106,9 @@ export default function ProcessInstanceRun({
const processModelRun = (processInstance: any) => {
setErrorMessage(null);
storeRecentProcessModelInLocalStorage(processModel);
HttpService.makeCallToBackend({
path: `/process-instances/${processInstance.id}/run`,
path: `/process-instances/${modifiedProcessModelId}/${processInstance.id}/run`,
successCallback: onProcessInstanceRun,
failureCallback: setErrorMessage,
httpMethod: 'POST',
@ -46,19 +117,23 @@ export default function ProcessInstanceRun({
const processInstanceCreateAndRun = () => {
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}/process-instances`,
path: processInstanceCreatePath,
successCallback: processModelRun,
httpMethod: 'POST',
});
};
if (checkPermissions) {
return (
<Can I="POST" a={processInstanceCreatePath} ability={ability}>
<Button onClick={processInstanceCreateAndRun} className={className}>
Start
</Button>
</Can>
);
}
return (
<Button
onClick={processInstanceCreateAndRun}
variant="primary"
className={className}
>
Run
<Button onClick={processInstanceCreateAndRun} className={className}>
Start
</Button>
);
}

View File

@ -1,10 +1,20 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Button,
ButtonSet,
Form,
Stack,
TextInput,
Grid,
Column,
// @ts-ignore
} from '@carbon/react';
// @ts-ignore
import { Button, ButtonSet, Form, Stack, TextInput } from '@carbon/react';
import { modifyProcessModelPath, slugifyString } from '../helpers';
import { AddAlt, TrashCan } from '@carbon/icons-react';
import { modifyProcessIdentifierForPathParam, slugifyString } from '../helpers';
import HttpService from '../services/HttpService';
import { ProcessModel } from '../interfaces';
import { MetadataExtractionPath, ProcessModel } from '../interfaces';
type OwnProps = {
mode: string;
@ -23,13 +33,13 @@ export default function ProcessModelForm({
const [idHasBeenUpdatedByUser, setIdHasBeenUpdatedByUser] =
useState<boolean>(false);
const [displayNameInvalid, setDisplayNameInvalid] = useState<boolean>(false);
useState<boolean>(false);
const navigate = useNavigate();
const navigateToProcessModel = (result: ProcessModel) => {
if ('id' in result) {
const modifiedProcessModelPathFromResult = modifyProcessModelPath(
result.id
);
const modifiedProcessModelPathFromResult =
modifyProcessIdentifierForPathParam(result.id);
navigate(`/admin/process-models/${modifiedProcessModelPathFromResult}`);
}
};
@ -52,14 +62,20 @@ export default function ProcessModelForm({
if (hasErrors) {
return;
}
const path = `/process-models/${processGroupId}`;
let path = `/process-models/${modifyProcessIdentifierForPathParam(
processGroupId || ''
)}`;
let httpMethod = 'POST';
if (mode === 'edit') {
httpMethod = 'PUT';
path = `/process-models/${modifyProcessIdentifierForPathParam(
processModel.id
)}`;
}
const postBody = {
display_name: processModel.display_name,
description: processModel.description,
metadata_extraction_paths: processModel.metadata_extraction_paths,
};
if (mode === 'new') {
Object.assign(postBody, {
@ -83,6 +99,80 @@ export default function ProcessModelForm({
setProcessModel(processModelToCopy);
};
const metadataExtractionPathForm = (
index: number,
metadataExtractionPath: MetadataExtractionPath
) => {
return (
<Grid>
<Column md={3} lg={7} sm={1}>
<TextInput
id={`process-model-metadata-extraction-path-key-${index}`}
labelText="Extraction Key"
value={metadataExtractionPath.key}
onChange={(event: any) => {
const cep: MetadataExtractionPath[] =
processModel.metadata_extraction_paths || [];
const newMeta = { ...metadataExtractionPath };
newMeta.key = event.target.value;
cep[index] = newMeta;
updateProcessModel({ metadata_extraction_paths: cep });
}}
/>
</Column>
<Column md={4} lg={8} sm={2}>
<TextInput
id={`process-model-metadata-extraction-path-${index}`}
labelText="Extraction Path"
value={metadataExtractionPath.path}
onChange={(event: any) => {
const cep: MetadataExtractionPath[] =
processModel.metadata_extraction_paths || [];
const newMeta = { ...metadataExtractionPath };
newMeta.path = event.target.value;
cep[index] = newMeta;
updateProcessModel({ metadata_extraction_paths: cep });
}}
/>
</Column>
<Column md={1} lg={1} sm={1}>
<Button
kind="ghost"
renderIcon={TrashCan}
iconDescription="Remove Key"
hasIconOnly
size="lg"
className="with-extra-top-margin"
onClick={() => {
const cep: MetadataExtractionPath[] =
processModel.metadata_extraction_paths || [];
cep.splice(index, 1);
updateProcessModel({ metadata_extraction_paths: cep });
}}
/>
</Column>
</Grid>
);
};
const metadataExtractionPathFormArea = () => {
if (processModel.metadata_extraction_paths) {
return processModel.metadata_extraction_paths.map(
(metadataExtractionPath: MetadataExtractionPath, index: number) => {
return metadataExtractionPathForm(index, metadataExtractionPath);
}
);
}
return null;
};
const addBlankMetadataExtractionPath = () => {
const cep: MetadataExtractionPath[] =
processModel.metadata_extraction_paths || [];
cep.push({ key: '', path: '' });
updateProcessModel({ metadata_extraction_paths: cep });
};
const onDisplayNameChanged = (newDisplayName: any) => {
setDisplayNameInvalid(false);
const updateDict = { display_name: newDisplayName };
@ -104,7 +194,6 @@ export default function ProcessModelForm({
onChange={(event: any) => {
onDisplayNameChanged(event.target.value);
}}
onBlur={(event: any) => console.log('event', event)}
/>,
];
@ -141,6 +230,38 @@ export default function ProcessModelForm({
/>
);
textInputs.push(<h2>Metadata Extractions</h2>);
textInputs.push(
<Grid>
<Column md={8} lg={16} sm={4}>
<p className="data-table-description">
You can provide one or more metadata extractions to pull data from
your process instances to provide quick access in searches and
perspectives.
</p>
</Column>
</Grid>
);
textInputs.push(<>{metadataExtractionPathFormArea()}</>);
textInputs.push(
<Grid>
<Column md={4} lg={8} sm={2}>
<Button
data-qa="add-metadata-extraction-path-button"
renderIcon={AddAlt}
className="button-white-background"
kind=""
size="sm"
onClick={() => {
addBlankMetadataExtractionPath();
}}
>
Add Metadata Extraction Path
</Button>
</Column>
</Grid>
);
return textInputs;
};

View File

@ -5,15 +5,24 @@ import {
// @ts-ignore
} from '@carbon/react';
import HttpService from '../services/HttpService';
import { ProcessModel, ProcessInstance } from '../interfaces';
import { modifyProcessModelPath, truncateString } from '../helpers';
import { ProcessModel, ProcessInstance, ProcessGroup } from '../interfaces';
import {
modifyProcessIdentifierForPathParam,
truncateString,
} from '../helpers';
import ProcessInstanceRun from './ProcessInstanceRun';
type OwnProps = {
headerElement?: ReactElement;
processGroup?: ProcessGroup;
checkPermissions?: boolean;
};
export default function ProcessModelListTiles({ headerElement }: OwnProps) {
export default function ProcessModelListTiles({
headerElement,
processGroup,
checkPermissions = true,
}: OwnProps) {
const [searchParams] = useSearchParams();
const [processModels, setProcessModels] = useState<ProcessModel[] | null>(
null
@ -25,13 +34,18 @@ export default function ProcessModelListTiles({ headerElement }: OwnProps) {
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';
// only allow 10 for now until we get the backend only returning certain models for user execution
let queryParams = '?per_page=20';
if (processGroup) {
queryParams = `${queryParams}&process_group_identifier=${processGroup.id}`;
} else {
queryParams = `${queryParams}&recursive=true&filter_runnable_by_user=true`;
}
HttpService.makeCallToBackend({
path: `/process-models${queryParams}`,
successCallback: setProcessModelsFromResult,
});
}, [searchParams]);
}, [searchParams, processGroup]);
const processInstanceRunResultTag = () => {
if (processInstance) {
@ -40,9 +54,9 @@ export default function ProcessModelListTiles({ headerElement }: OwnProps) {
<p>
Process Instance {processInstance.id} kicked off (
<Link
to={`/admin/process-models/${modifyProcessModelPath(
to={`/admin/process-instances/${modifyProcessIdentifierForPathParam(
processInstance.process_model_identifier
)}/process-instances/${processInstance.id}`}
)}/${processInstance.id}`}
data-qa="process-instance-show-link"
>
view
@ -61,19 +75,29 @@ export default function ProcessModelListTiles({ headerElement }: OwnProps) {
displayText = (processModels || []).map((row: ProcessModel) => {
return (
<Tile
id="tile-1"
id={`process-model-tile-${row.id}`}
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>
<div className="tile-title-top">
<a
title={row.id}
data-qa="process-model-show-link"
href={`/admin/process-models/${modifyProcessIdentifierForPathParam(
row.id
)}`}
>
{row.display_name}
</a>
</div>
<p className="tile-description">
{truncateString(row.description || '', 25)}
{truncateString(row.description || '', 100)}
</p>
<ProcessInstanceRun
processModel={row}
onSuccessCallback={setProcessInstance}
className="tile-pin-bottom"
checkPermissions={checkPermissions}
/>
</div>
</Tile>

View File

@ -35,7 +35,7 @@ export default function ProcessModelSearch({
if (processModel) {
return `${processModel.id} (${truncateString(
processModel.display_name,
20
75
)})`;
}
return null;

View File

@ -41,7 +41,7 @@ export default function ProcessSearch({
if (process) {
return `${process.display_name} (${truncateString(
process.identifier,
20
75
)})`;
}
return null;

View File

@ -429,7 +429,7 @@ export default function ReactDiagramEditor({
fetch(urlToUse)
.then((response) => response.text())
.then((text) => {
const processId = `Proccess_${makeid(7)}`;
const processId = `Process_${makeid(7)}`;
const newText = text.replace('{{PROCESS_ID}}', processId);
setDiagramXMLString(newText);
})
@ -569,7 +569,7 @@ export default function ReactDiagramEditor({
a={targetUris.processModelFileShowPath}
ability={ability}
>
<Button onClick={downloadXmlFile}>Download xml</Button>
<Button onClick={downloadXmlFile}>Download</Button>
</Can>
</>
);

View File

@ -0,0 +1,17 @@
// @ts-ignore
import { TimeAgo } from '../helpers/timeago';
import { convertSecondsToFormattedDateTime } from '../helpers';
type OwnProps = {
timeInSeconds: number;
};
export default function TableCellWithTimeAgoInWords({
timeInSeconds,
}: OwnProps) {
return (
<td title={convertSecondsToFormattedDateTime(timeInSeconds) || '-'}>
{timeInSeconds ? TimeAgo.inWords(timeInSeconds) : '-'}
</td>
);
}

View File

@ -6,13 +6,17 @@ import PaginationForTable from './PaginationForTable';
import {
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessModelPath,
modifyProcessIdentifierForPathParam,
refreshAtInterval,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces';
import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const paginationQueryParamPrefix = 'tasks_for_my_open_processes';
const REFRESH_INTERVAL = 5;
const REFRESH_TIMEOUT = 600;
export default function MyOpenProcesses() {
const [searchParams] = useSearchParams();
@ -20,45 +24,50 @@ export default function MyOpenProcesses() {
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);
const getTasks = () => {
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,
});
};
HttpService.makeCallToBackend({
path: `/tasks/for-my-open-processes?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
getTasks();
refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks);
}, [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
);
const modifiedProcessModelIdentifier =
modifyProcessIdentifierForPathParam(rowToUse.process_model_identifier);
return (
<tr key={rowToUse.id}>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
data-qa="process-instance-show-link"
to={`/admin/process-instances/${modifiedProcessModelIdentifier}/${rowToUse.process_instance_id}`}
title={`View process instance ${rowToUse.process_instance_id}`}
>
{rowToUse.process_model_display_name}
{rowToUse.process_instance_id}
</Link>
</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
title={rowToUse.process_model_identifier}
>
View {rowToUse.process_instance_id}
{rowToUse.process_model_display_name}
</Link>
</td>
<td
@ -66,18 +75,15 @@ export default function MyOpenProcesses() {
>
{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>
<TableCellWithTimeAgoInWords
timeInSeconds={rowToUse.updated_at_in_seconds}
/>
<td>
<Button
variant="primary"
@ -95,13 +101,12 @@ export default function MyOpenProcesses() {
<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>Id</th>
<th>Process</th>
<th>Task</th>
<th>Waiting For</th>
<th>Date Started</th>
<th>Last Updated</th>
<th>Actions</th>
</tr>
</thead>
@ -112,7 +117,11 @@ export default function MyOpenProcesses() {
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return null;
return (
<p className="no-results-message with-large-bottom-margin">
There are no tasks for processes you started at this time.
</p>
);
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
@ -121,22 +130,27 @@ export default function MyOpenProcesses() {
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}
/>
</>
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix={paginationQueryParamPrefix}
paginationClassName="with-large-bottom-margin"
/>
);
};
if (pagination) {
return tasksComponent();
}
return null;
return (
<>
<h2>My open instances</h2>
<p className="data-table-description">
These tasks are for processes you started which are not complete. You
may not have an action to take at this time. See below for tasks waiting
on you.
</p>
{tasksComponent()}
</>
);
}

View File

@ -6,10 +6,11 @@ import PaginationForTable from './PaginationForTable';
import {
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessModelPath,
modifyProcessIdentifierForPathParam,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces';
import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
@ -39,25 +40,26 @@ export default function TasksWaitingForMe() {
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
);
const modifiedProcessModelIdentifier =
modifyProcessIdentifierForPathParam(rowToUse.process_model_identifier);
return (
<tr key={rowToUse.id}>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
data-qa="process-instance-show-link"
to={`/admin/${modifiedProcessModelIdentifier}/${rowToUse.process_instance_id}`}
title={`View process instance ${rowToUse.process_instance_id}`}
>
{rowToUse.process_model_display_name}
{rowToUse.process_instance_id}
</Link>
</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
title={rowToUse.process_model_identifier}
>
View {rowToUse.process_instance_id}
{rowToUse.process_model_display_name}
</Link>
</td>
<td
@ -66,18 +68,15 @@ export default function TasksWaitingForMe() {
{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>
<TableCellWithTimeAgoInWords
timeInSeconds={rowToUse.updated_at_in_seconds}
/>
<td>
<Button
variant="primary"
@ -95,14 +94,13 @@ export default function TasksWaitingForMe() {
<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>Id</th>
<th>Process</th>
<th>Task</th>
<th>Started By</th>
<th>Waiting For</th>
<th>Date Started</th>
<th>Last Updated</th>
<th>Actions</th>
</tr>
</thead>
@ -113,7 +111,11 @@ export default function TasksWaitingForMe() {
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return null;
return (
<p className="no-results-message with-large-bottom-margin">
You have no task assignments at this time.
</p>
);
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
@ -122,22 +124,26 @@ export default function TasksWaitingForMe() {
'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"
/>
</>
<PaginationForTable
page={page}
perPage={perPage}
perPageOptions={[2, PER_PAGE_FOR_TASKS_ON_HOME_PAGE, 25]}
pagination={pagination}
tableToDisplay={buildTable()}
paginationQueryParamPrefix="tasks_waiting_for_me"
paginationClassName="with-large-bottom-margin"
/>
);
};
if (pagination) {
return tasksComponent();
}
return null;
return (
<>
<h2>Tasks waiting for me</h2>
<p className="data-table-description">
These processes are waiting on you to complete the next task. All are
processes created by others that are now actionable by you.
</p>
{tasksComponent()}
</>
);
}

View File

@ -6,59 +6,68 @@ import PaginationForTable from './PaginationForTable';
import {
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessModelPath,
modifyProcessIdentifierForPathParam,
refreshAtInterval,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject } from '../interfaces';
import TableCellWithTimeAgoInWords from './TableCellWithTimeAgoInWords';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const paginationQueryParamPrefix = 'tasks_waiting_for_my_groups';
const REFRESH_INTERVAL = 5;
const REFRESH_TIMEOUT = 600;
export default function TasksForWaitingForMyGroups() {
export default function TasksWaitingForMyGroups() {
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);
const getTasks = () => {
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,
});
};
HttpService.makeCallToBackend({
path: `/tasks/for-my-groups?per_page=${perPage}&page=${page}`,
successCallback: setTasksFromResult,
});
getTasks();
refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks);
}, [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
);
const modifiedProcessModelIdentifier =
modifyProcessIdentifierForPathParam(rowToUse.process_model_identifier);
return (
<tr key={rowToUse.id}>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
data-qa="process-instance-show-link"
to={`/admin/process-instances/${modifiedProcessModelIdentifier}/${rowToUse.process_instance_id}`}
title={`View process instance ${rowToUse.process_instance_id}`}
>
{rowToUse.process_model_display_name}
{rowToUse.process_instance_id}
</Link>
</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}`}
title={rowToUse.process_model_identifier}
>
View {rowToUse.process_instance_id}
{rowToUse.process_model_display_name}
</Link>
</td>
<td
@ -67,18 +76,15 @@ export default function TasksForWaitingForMyGroups() {
{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>
<TableCellWithTimeAgoInWords
timeInSeconds={rowToUse.updated_at_in_seconds}
/>
<td>
<Button
variant="primary"
@ -96,14 +102,13 @@ export default function TasksForWaitingForMyGroups() {
<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>Id</th>
<th>Process</th>
<th>Task</th>
<th>Started By</th>
<th>Waiting For</th>
<th>Date Started</th>
<th>Last Updated</th>
<th>Actions</th>
</tr>
</thead>
@ -114,7 +119,11 @@ export default function TasksForWaitingForMyGroups() {
const tasksComponent = () => {
if (pagination && pagination.total < 1) {
return null;
return (
<p className="no-results-message">
Your groups have no task assignments at this time.
</p>
);
}
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
@ -123,22 +132,25 @@ export default function TasksForWaitingForMyGroups() {
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}
/>
</>
<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;
return (
<>
<h2>Tasks waiting for my groups</h2>
<p className="data-table-description">
This is a list of tasks for groups you belong to that can be completed
by any member of the group.
</p>
{tasksComponent()}
</>
);
}

View File

@ -14,6 +14,7 @@ export const PROCESS_STATUSES = [
'complete',
'error',
'suspended',
'terminated',
];
// with time: yyyy-MM-dd HH:mm:ss

View File

@ -174,18 +174,18 @@ export const getProcessModelFullIdentifierFromSearchParams = (
// https://stackoverflow.com/a/71352046/6090676
export const truncateString = (text: string, len: number) => {
if (text.length > len && text.length > 0) {
return `${text.split(' ').slice(0, len).join(' ')} ...`;
return `${text.split('').slice(0, len).join('')} ...`;
}
return text;
};
// Because of limitations in the way openapi defines parameters, we have to modify process models ids
// which are basically paths to the models
export const modifyProcessModelPath = (path: string) => {
export const modifyProcessIdentifierForPathParam = (path: string) => {
return path.replace(/\//g, ':') || '';
};
export const unModifyProcessModelPath = (path: string) => {
export const unModifyProcessIdentifierForPathParam = (path: string) => {
return path.replace(/:/g, '/') || '';
};

62
src/helpers/timeago.js Normal file
View File

@ -0,0 +1,62 @@
/* eslint-disable no-restricted-syntax */
// https://gist.github.com/caiotarifa/30ae974f2293c761f3139dd194abd9e5
export const TimeAgo = (function awesomeFunc() {
const self = {};
// Public Methods
self.locales = {
prefix: '',
sufix: 'ago',
seconds: 'less than a minute',
minute: 'about a minute',
minutes: '%d minutes',
hour: 'about an hour',
hours: 'about %d hours',
day: 'a day',
days: '%d days',
month: 'about a month',
months: '%d months',
year: 'about a year',
years: '%d years',
};
self.inWords = function inWords(timeAgo) {
const milliseconds = timeAgo * 1000;
const seconds = Math.floor(
(new Date() - parseInt(milliseconds, 10)) / 1000
);
const separator = this.locales.separator || ' ';
let words = this.locales.prefix + separator;
let interval = 0;
const intervals = {
year: seconds / 31536000,
month: seconds / 2592000,
day: seconds / 86400,
hour: seconds / 3600,
minute: seconds / 60,
};
let distance = this.locales.seconds;
// eslint-disable-next-line guard-for-in
for (const key in intervals) {
interval = Math.floor(intervals[key]);
if (interval > 1) {
distance = this.locales[`${key}s`];
break;
} else if (interval === 1) {
distance = this.locales[key];
break;
}
}
distance = distance.replace(/%d/i, interval);
words += distance + separator + this.locales.sufix;
return words.trim();
};
return self;
})();

View File

@ -1,7 +1,7 @@
// 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 { useContext, useEffect, useState } from 'react';
import { AbilityContext } from '../contexts/Can';
import { PermissionCheckResponseBody, PermissionsToCheck } from '../interfaces';
import HttpService from '../services/HttpService';
@ -10,6 +10,7 @@ export const usePermissionFetcher = (
permissionsToCheck: PermissionsToCheck
) => {
const ability = useContext(AbilityContext);
const [permissionsLoaded, setPermissionsLoaded] = useState<boolean>(false);
useEffect(() => {
const processPermissionResult = (result: PermissionCheckResponseBody) => {
@ -34,15 +35,17 @@ export const usePermissionFetcher = (
}
});
ability.update(rules);
setPermissionsLoaded(true);
};
HttpService.makeCallToBackend({
path: `/permissions-check`,
httpMethod: 'POST',
successCallback: processPermissionResult,
postBody: { requests_to_check: permissionsToCheck },
});
if (Object.keys(permissionsToCheck).length !== 0) {
HttpService.makeCallToBackend({
path: `/permissions-check`,
httpMethod: 'POST',
successCallback: processPermissionResult,
postBody: { requests_to_check: permissionsToCheck },
});
}
});
return { ability };
return { ability, permissionsLoaded };
};

View File

@ -1,20 +1,28 @@
import { useMemo } from 'react';
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`,
};
const targetUris = useMemo(() => {
return {
authenticationListPath: `/v1.0/authentications`,
messageInstanceListPath: '/v1.0/messages',
processGroupListPath: '/v1.0/process-groups',
processGroupShowPath: `/v1.0/process-groups/${params.process_group_id}`,
processInstanceCreatePath: `/v1.0/process-instances/${params.process_model_id}`,
processInstanceActionPath: `/v1.0/process-instances/${params.process_model_id}/${params.process_instance_id}`,
processInstanceListPath: '/v1.0/process-instances',
processInstanceLogListPath: `/v1.0/logs/${params.process_model_id}/${params.process_instance_id}`,
processInstanceReportListPath: '/v1.0/process-instances/reports',
processInstanceTaskListPath: `/v1.0/task-data/${params.process_model_id}/${params.process_instance_id}`,
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}`,
processModelPublishPath: `/v1.0/process-models/${params.process_model_id}/publish`,
processModelShowPath: `/v1.0/process-models/${params.process_model_id}`,
secretListPath: `/v1.0/secrets`,
};
}, [params]);
return { targetUris };
};

View File

@ -5,6 +5,15 @@
color: white;
}
.megacondensed {
padding-left: 0px;
}
/* defaults to 3rem, which isn't long sufficient for "elizabeth" */
.cds--header__action.username-header-text {
width: 5rem;
}
h1 {
font-weight: 400;
font-size: 28px;
@ -60,6 +69,24 @@ h2 {
color: black;
}
/* match normal link colors */
.cds--btn--ghost.button-link {
color: #0062fe;
padding-left: 0;
}
.cds--btn--ghost.button-link:visited {
color: #0062fe;
padding-left: 0;
}
.cds--btn--ghost.button-link:hover {
color: #0062fe;
padding-left: 0;
}
.cds--btn--ghost.button-link:visited:hover {
color: #0062fe;
padding-left: 0;
}
.cds--header__global .cds--btn--primary {
background-color: #161616
}
@ -138,6 +165,26 @@ h1.with-icons {
margin-bottom: 1em;
}
.with-top-margin {
margin-top: 1em;
}
.with-extra-top-margin {
margin-top: 1.3em;
}
.with-tiny-top-margin {
margin-top: 4px;
}
.with-large-bottom-margin {
margin-bottom: 3em;
}
.with-tiny-bottom-margin {
margin-bottom: 4px;
}
.diagram-viewer-canvas {
border:1px solid #000000;
height:70vh;
@ -243,3 +290,83 @@ in on this with the react-jsonschema-form repo. This is just a patch fix to allo
position: absolute;
bottom: 1em;
}
.cds--tabs .cds--tabs__nav-link {
max-width: 20rem;
}
.clear-left {
clear: left;
}
td.actions-cell {
width: 1em;
}
.no-results-message {
font-style: italic;
margin-left: 2em;
margin-top: 1em;
font-size: 14px;
}
.data-table-description {
font-size: 14px;
line-height: 18px;
letter-spacing: 0.16px;
color: #525252;
margin-bottom: 1em;
}
/* top and bottom margin since this is sort of the middle of three sections on the process model show page */
.process-model-files-section {
margin: 2em 0;
}
.filterIcon {
text-align: right;
padding-bottom: 10px;
}
.cds--btn--ghost:not([disabled]) svg.red-icon {
fill: red;
}
svg.green-icon {
fill: #198038;
}
svg.notification-icon {
margin-right: 1rem;
}
.failure-string {
color: red;
}
.cds--btn--ghost.cds--btn--sm.button-tag-icon {
padding-left: 0;
padding-right: 0;
padding-top: 0;
}
/* .no-wrap cds--label cds--label--inline cds--label--inline--md{ */
.no-wrap .cds--label--inline{
word-break: normal;
}
.combo-box-in-modal {
height: 300px;
}
.cds--btn.narrow-button {
max-width: 10rem;
min-width: 5rem;
word-break: normal;
}
/* lime green */
.tag-type-green:hover {
background-color: #80ee90;
}

View File

@ -12,9 +12,8 @@ export interface RecentProcessModel {
}
export interface ProcessReference {
id: string; // The unique id of the process or decision table.
name: string; // The process or decision Display name.
identifier: string;
identifier: string; // The unique id of the process
display_name: string;
process_group_id: string;
process_model_id: string;
@ -39,6 +38,68 @@ export interface ProcessFile {
export interface ProcessInstance {
id: number;
process_model_identifier: string;
process_model_display_name: string;
}
export interface MessageCorrelationProperties {
[key: string]: string;
}
export interface MessageCorrelations {
[key: string]: MessageCorrelationProperties;
}
export interface MessageInstance {
id: number;
process_model_identifier: string;
process_model_display_name: string;
process_instance_id: number;
message_identifier: string;
message_type: string;
failure_cause: string;
status: string;
created_at_in_seconds: number;
message_correlations?: MessageCorrelations;
}
export interface ReportFilter {
field_name: string;
field_value: string;
operator?: string;
}
export interface ReportColumn {
Header: string;
accessor: string;
filterable: boolean;
}
export interface ReportColumnForEditing extends ReportColumn {
filter_field_value: string;
filter_operator: string;
}
export interface ReportMetadata {
columns: ReportColumn[];
filter_by: ReportFilter[];
order_by: string[];
}
export interface ProcessInstanceReport {
id: number;
identifier: string;
name: string;
report_metadata: ReportMetadata;
}
export interface ProcessGroupLite {
id: string;
display_name: string;
}
export interface MetadataExtractionPath {
key: string;
path: string;
}
export interface ProcessModel {
@ -47,6 +108,8 @@ export interface ProcessModel {
display_name: string;
primary_file_name: string;
files: ProcessFile[];
parent_groups?: ProcessGroupLite[];
metadata_extraction_paths?: MetadataExtractionPath[];
}
export interface ProcessGroup {
@ -55,10 +118,19 @@ export interface ProcessGroup {
description?: string | null;
process_models?: ProcessModel[];
process_groups?: ProcessGroup[];
parent_groups?: ProcessGroupLite[];
}
export interface HotCrumbItemObject {
entityToExplode: ProcessModel | ProcessGroup | string;
entityType: string;
linkLastItem?: boolean;
}
export type HotCrumbItemArray = [displayValue: string, url?: string];
// tuple of display value and URL
export type HotCrumbItem = [displayValue: string, url?: string];
export type HotCrumbItem = HotCrumbItemArray | HotCrumbItemObject;
export interface ErrorForDisplay {
message: string;

View File

@ -71,11 +71,11 @@ export default function AdminRoutes() {
element={<ProcessModelEdit />}
/>
<Route
path="process-models/:process_model_id/process-instances/:process_instance_id"
path="process-instances/:process_model_id/:process_instance_id"
element={<ProcessInstanceShow />}
/>
<Route
path="process-models/:process_model_id/process-instances/:process_instance_id/:spiff_step"
path="process-instances/:process_model_id/:process_instance_id/:spiff_step"
element={<ProcessInstanceShow />}
/>
<Route
@ -103,7 +103,7 @@ export default function AdminRoutes() {
element={<ReactFormEditor />}
/>
<Route
path="process-models/:process_model_id/process-instances/:process_instance_id/logs"
path="logs/:process_model_id/:process_instance_id"
element={<ProcessInstanceLogList />}
/>
<Route path="process-instances" element={<ProcessInstanceList />} />

View File

@ -54,7 +54,7 @@ export default function AuthenticationList() {
<Table striped bordered>
<thead>
<tr>
<th>ID</th>
<th>Id</th>
</tr>
</thead>
<tbody>{rows}</tbody>

View File

@ -1,5 +1,48 @@
import MyCompletedInstances from '../components/MyCompletedInstances';
import ProcessInstanceListTable from '../components/ProcessInstanceListTable';
export default function CompletedInstances() {
return <MyCompletedInstances />;
return (
<>
<h2>My completed instances</h2>
<p className="data-table-description">
This is a list of instances you started that are now complete.
</p>
<ProcessInstanceListTable
filtersEnabled={false}
paginationQueryParamPrefix="my_completed_instances"
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_instances_initiated_by_me"
showReports={false}
textToShowIfEmpty="You have no completed instances at this time."
paginationClassName="with-large-bottom-margin"
autoReload
/>
<h2>Tasks completed by me</h2>
<p className="data-table-description">
This is a list of instances where you have completed tasks.
</p>
<ProcessInstanceListTable
filtersEnabled={false}
paginationQueryParamPrefix="my_completed_tasks"
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_instances_with_tasks_completed_by_me"
showReports={false}
textToShowIfEmpty="You have no completed tasks at this time."
paginationClassName="with-large-bottom-margin"
/>
<h2>Tasks completed by my groups</h2>
<p className="data-table-description">
This is a list of instances with tasks that were completed by groups you
belong to.
</p>
<ProcessInstanceListTable
filtersEnabled={false}
paginationQueryParamPrefix="group_completed_tasks"
perPageOptions={[2, 5, 25]}
reportIdentifier="system_report_instances_with_tasks_completed_by_my_groups"
showReports={false}
textToShowIfEmpty="Your group has no completed tasks at this time."
/>
</>
);
}

View File

@ -3,7 +3,8 @@ import ProcessModelListTiles from '../components/ProcessModelListTiles';
export default function CreateNewInstance() {
return (
<ProcessModelListTiles
headerElement={<h1>Process models available to you</h1>}
headerElement={<h2>Processes I can start</h2>}
checkPermissions={false}
/>
);
}

View File

@ -1,15 +1,15 @@
import TasksForMyOpenProcesses from '../components/TasksForMyOpenProcesses';
import TasksWaitingForMe from '../components/TasksWaitingForMe';
import TasksForWaitingForMyGroups from '../components/TasksWaitingForMyGroups';
import TasksWaitingForMyGroups from '../components/TasksWaitingForMyGroups';
export default function GroupedTasks() {
return (
<>
{/* be careful moving these around since the first two have with-large-bottom-margin in order to get some space between the three table sections. */}
{/* i wish Stack worked to add space just between top-level elements */}
<TasksForMyOpenProcesses />
<br />
<TasksWaitingForMe />
<br />
<TasksForWaitingForMyGroups />
<TasksWaitingForMyGroups />
</>
);
}

View File

@ -18,12 +18,10 @@ export default function HomePageRoutes() {
useEffect(() => {
setErrorMessage(null);
let newSelectedTabIndex = 0;
if (location.pathname.match(/^\/tasks\/grouped\b/)) {
if (location.pathname.match(/^\/tasks\/completed-instances\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;
newSelectedTabIndex = 2;
}
setSelectedTabIndex(newSelectedTabIndex);
}, [location, setErrorMessage]);
@ -36,13 +34,13 @@ export default function HomePageRoutes() {
<>
<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/my-tasks')}>My Tasks</Tab> */}
<Tab onClick={() => navigate('/tasks/grouped')}>In Progress</Tab>
<Tab onClick={() => navigate('/tasks/completed-instances')}>
Completed Instances
Completed
</Tab>
<Tab onClick={() => navigate('/tasks/create-new-instance')}>
Create New Instance +
Start New +
</Tab>
</TabList>
</Tabs>
@ -55,7 +53,7 @@ export default function HomePageRoutes() {
<>
{renderTabs()}
<Routes>
<Route path="/" element={<MyTasks />} />
<Route path="/" element={<GroupedTasks />} />
<Route path="my-tasks" element={<MyTasks />} />
<Route path=":process_instance_id/:task_id" element={<TaskShow />} />
<Route path="grouped" element={<GroupedTasks />} />

View File

@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
import { Button, Select, SelectItem, TextInput } from '@carbon/react';
import { useParams } from 'react-router-dom';
import { FormField } from '../interfaces';
import { modifyProcessModelPath, slugifyString } from '../helpers';
import { modifyProcessIdentifierForPathParam, slugifyString } from '../helpers';
import HttpService from '../services/HttpService';
export default function JsonSchemaFormBuilder() {
@ -28,7 +28,7 @@ export default function JsonSchemaFormBuilder() {
const [formFieldTitle, setFormFieldTitle] = useState<string>('');
const [formFieldType, setFormFieldType] = useState<string>('');
const modifiedProcessModelId = modifyProcessModelPath(
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
`${params.process_model_id}`
);

View File

@ -1,16 +1,19 @@
import { useEffect, useState } from 'react';
// @ts-ignore
import { Table } from '@carbon/react';
import { ErrorOutline } from '@carbon/icons-react';
// @ts-ignore
import { Table, Modal, Button } from '@carbon/react';
import { Link, useParams, useSearchParams } from 'react-router-dom';
import PaginationForTable from '../components/PaginationForTable';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import {
convertSecondsToFormattedDateString,
convertSecondsToFormattedDateTime,
getPageInfoFromSearchParams,
modifyProcessModelPath,
unModifyProcessModelPath,
modifyProcessIdentifierForPathParam,
} from '../helpers';
import HttpService from '../services/HttpService';
import { FormatProcessModelDisplayName } from '../components/MiniComponents';
import { MessageInstance } from '../interfaces';
export default function MessageInstanceList() {
const params = useParams();
@ -18,6 +21,9 @@ export default function MessageInstanceList() {
const [messageIntances, setMessageInstances] = useState([]);
const [pagination, setPagination] = useState(null);
const [messageInstanceForModal, setMessageInstanceForModal] =
useState<MessageInstance | null>(null);
useEffect(() => {
const setMessageInstanceListFromResult = (result: any) => {
setMessageInstances(result.results);
@ -36,41 +42,89 @@ export default function MessageInstanceList() {
});
}, [searchParams, params]);
const buildTable = () => {
// return null;
const rows = messageIntances.map((row) => {
const rowToUse = row as any;
const handleCorrelationDisplayClose = () => {
setMessageInstanceForModal(null);
};
const correlationsDisplayModal = () => {
if (messageInstanceForModal) {
let failureCausePre = null;
if (messageInstanceForModal.failure_cause) {
failureCausePre = (
<>
<p className="failure-string">
{messageInstanceForModal.failure_cause}
</p>
<br />
</>
);
}
return (
<tr key={rowToUse.id}>
<td>{rowToUse.id}</td>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifyProcessModelPath(
rowToUse.process_model_identifier
)}`}
>
{rowToUse.process_model_identifier}
</Link>
</td>
<Modal
open={!!messageInstanceForModal}
passiveModal
onRequestClose={handleCorrelationDisplayClose}
modalHeading={`Message ${messageInstanceForModal.id} (${messageInstanceForModal.message_identifier}) ${messageInstanceForModal.message_type} data:`}
modalLabel="Details"
>
{failureCausePre}
<p>Correlations:</p>
<pre>
{JSON.stringify(
messageInstanceForModal.message_correlations,
null,
2
)}
</pre>
</Modal>
);
}
return null;
};
const buildTable = () => {
const rows = messageIntances.map((row: MessageInstance) => {
let errorIcon = null;
let errorTitle = null;
if (row.failure_cause) {
errorTitle = 'Instance has an error';
errorIcon = (
<>
&nbsp;
<ErrorOutline className="red-icon" />
</>
);
}
return (
<tr key={row.id}>
<td>{row.id}</td>
<td>{FormatProcessModelDisplayName(row)}</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifyProcessModelPath(
rowToUse.process_model_identifier
)}/process-instances/${rowToUse.process_instance_id}`}
to={`/admin/process-instances/${modifyProcessIdentifierForPathParam(
row.process_model_identifier
)}/${row.process_instance_id}`}
>
{rowToUse.process_instance_id}
{row.process_instance_id}
</Link>
</td>
<td>{rowToUse.message_identifier}</td>
<td>{rowToUse.message_type}</td>
<td>{rowToUse.failure_cause || '-'}</td>
<td>{rowToUse.status}</td>
<td>{row.message_identifier}</td>
<td>{row.message_type}</td>
<td>
{convertSecondsToFormattedDateString(
rowToUse.created_at_in_seconds
)}
<Button
kind="ghost"
className="button-link"
onClick={() => setMessageInstanceForModal(row)}
title={errorTitle}
>
View
{errorIcon}
</Button>
</td>
<td>{row.status}</td>
<td>
{convertSecondsToFormattedDateTime(row.created_at_in_seconds)}
</td>
</tr>
);
@ -79,12 +133,12 @@ export default function MessageInstanceList() {
<Table striped bordered>
<thead>
<tr>
<th>Instance Id</th>
<th>Process Model</th>
<th>Id</th>
<th>Process</th>
<th>Process Instance</th>
<th>Message Model</th>
<th>Name</th>
<th>Type</th>
<th>Failure Cause</th>
<th>Details</th>
<th>Status</th>
<th>Created At</th>
</tr>
@ -102,17 +156,16 @@ export default function MessageInstanceList() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Model: ${params.process_model_id}`,
`process_model:${unModifyProcessModelPath(
searchParams.get('process_model_id') || ''
)}:link`,
],
{
entityToExplode: searchParams.get('process_model_id') || '',
entityType: 'process-model-id',
linkLastItem: true,
},
[
`Process Instance: ${searchParams.get('process_instance_id')}`,
`/admin/process-models/${searchParams.get(
`/admin/process-instances/${searchParams.get(
'process_model_id'
)}/process-instances/${searchParams.get('process_instance_id')}`,
)}/${searchParams.get('process_instance_id')}`,
],
['Messages'],
]}
@ -123,6 +176,7 @@ export default function MessageInstanceList() {
<>
{breadcrumbElement}
<h1>Messages</h1>
{correlationsDisplayModal()}
<PaginationForTable
page={page}
perPage={perPage}

View File

@ -5,20 +5,28 @@ import { Link, useSearchParams } from 'react-router-dom';
import PaginationForTable from '../components/PaginationForTable';
import {
getPageInfoFromSearchParams,
modifyProcessModelPath,
modifyProcessIdentifierForPathParam,
refreshAtInterval,
} from '../helpers';
import HttpService from '../services/HttpService';
import { PaginationObject, RecentProcessModel } from '../interfaces';
import {
PaginationObject,
ProcessInstance,
ProcessModel,
RecentProcessModel,
} from '../interfaces';
import ProcessInstanceRun from '../components/ProcessInstanceRun';
const PER_PAGE_FOR_TASKS_ON_HOME_PAGE = 5;
const REFRESH_INTERVAL = 10;
const REFRESH_INTERVAL = 5;
const REFRESH_TIMEOUT = 600;
export default function MyTasks() {
const [searchParams] = useSearchParams();
const [tasks, setTasks] = useState([]);
const [pagination, setPagination] = useState<PaginationObject | null>(null);
const [processInstance, setProcessInstance] =
useState<ProcessInstance | null>(null);
useEffect(() => {
const getTasks = () => {
@ -40,6 +48,28 @@ export default function MyTasks() {
refreshAtInterval(REFRESH_INTERVAL, REFRESH_TIMEOUT, getTasks);
}, [searchParams]);
const processInstanceRunResultTag = () => {
if (processInstance) {
return (
<div className="alert alert-success" role="alert">
<p>
Process Instance {processInstance.id} kicked off (
<Link
to={`/admin/process-instances/${modifyProcessIdentifierForPathParam(
processInstance.process_model_identifier
)}/${processInstance.id}`}
data-qa="process-instance-show-link"
>
view
</Link>
).
</p>
</div>
);
}
return null;
};
let recentProcessModels: RecentProcessModel[] = [];
const recentProcessModelsString = localStorage.getItem('recentProcessModels');
if (recentProcessModelsString !== null) {
@ -50,9 +80,8 @@ export default function MyTasks() {
const rows = tasks.map((row) => {
const rowToUse = row as any;
const taskUrl = `/tasks/${rowToUse.process_instance_id}/${rowToUse.id}`;
const modifiedProcessModelIdentifier = modifyProcessModelPath(
rowToUse.process_model_identifier
);
const modifiedProcessModelIdentifier =
modifyProcessIdentifierForPathParam(rowToUse.process_model_identifier);
return (
<tr key={rowToUse.id}>
<td>
@ -66,9 +95,9 @@ export default function MyTasks() {
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelIdentifier}/process-instances/${rowToUse.process_instance_id}`}
to={`/admin/process-instances/${modifiedProcessModelIdentifier}/${rowToUse.process_instance_id}`}
>
View {rowToUse.process_instance_id}
{rowToUse.process_instance_id}
</Link>
</td>
<td
@ -108,33 +137,44 @@ export default function MyTasks() {
};
const buildRecentProcessModelSection = () => {
const rows = recentProcessModels.map((row) => {
const rowToUse = row as any;
const modifiedProcessModelId = modifyProcessModelPath(
rowToUse.processModelIdentifier
const rows = recentProcessModels.map((row: RecentProcessModel) => {
const processModel: ProcessModel = {
id: row.processModelIdentifier,
description: '',
display_name: '',
primary_file_name: '',
files: [],
};
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
row.processModelIdentifier
);
return (
<tr
key={`${rowToUse.processGroupIdentifier}/${rowToUse.processModelIdentifier}`}
>
<tr key={`${row.processGroupIdentifier}/${row.processModelIdentifier}`}>
<td>
<Link
data-qa="process-model-show-link"
to={`/admin/process-models/${modifiedProcessModelId}`}
>
{rowToUse.processModelDisplayName}
{row.processModelDisplayName}
</Link>
</td>
<td className="actions-cell">
<ProcessInstanceRun
processModel={processModel}
onSuccessCallback={setProcessInstance}
/>
</td>
</tr>
);
});
return (
<>
<h1>Recently viewed process models</h1>
<h1>Recently instantiated process models</h1>
<Table striped bordered>
<thead>
<tr>
<th>Process Model</th>
<th>Actions</th>
</tr>
</thead>
<tbody>{rows}</tbody>
@ -176,6 +216,7 @@ export default function MyTasks() {
}
return (
<>
{processInstanceRunResultTag()}
{tasksWaitingForMe}
<br />
{relevantProcessModelSection}

View File

@ -27,10 +27,11 @@ export default function ProcessGroupEdit() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Group: ${processGroup.id}:link`,
`process_group:${processGroup.id}:link`,
],
{
entityToExplode: processGroup,
entityType: 'process-group',
linkLastItem: true,
},
]}
/>
<h1>Edit Process Group: {(processGroup as any).id}</h1>

View File

@ -7,7 +7,7 @@ import {
import { Can } from '@casl/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import HttpService from '../services/HttpService';
import { modifyProcessModelPath } from '../helpers';
import { modifyProcessIdentifierForPathParam } from '../helpers';
import { CarbonComboBoxSelection, PermissionsToCheck } from '../interfaces';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { usePermissionFetcher } from '../hooks/PermissionService';
@ -39,7 +39,7 @@ export default function ProcessGroupList() {
};
// for search box
HttpService.makeCallToBackend({
path: `/process-models?per_page=1000`,
path: `/process-models?per_page=1000&recursive=true`,
successCallback: processResultForProcessModels,
});
}, [searchParams]);
@ -48,7 +48,9 @@ export default function ProcessGroupList() {
const processModelSearchOnChange = (selection: CarbonComboBoxSelection) => {
const processModel = selection.selectedItem;
navigate(
`/admin/process-models/${modifyProcessModelPath(processModel.id)}`
`/admin/process-models/${modifyProcessIdentifierForPathParam(
processModel.id
)}`
);
};
return (

View File

@ -14,7 +14,11 @@ export default function ProcessGroupNew() {
const hotCrumbs: HotCrumbItem[] = [['Process Groups', '/admin']];
if (parentGroupId) {
hotCrumbs.push(['', `process_group:${parentGroupId}:link`]);
hotCrumbs.push({
entityToExplode: parentGroupId,
entityType: 'process-group-id',
linkLastItem: true,
});
}
return (

View File

@ -1,39 +1,51 @@
import { useEffect, useState } from 'react';
import { Link, useSearchParams, useParams } from 'react-router-dom';
import {
// Link,
useSearchParams,
useParams,
useNavigate,
} from 'react-router-dom';
import {
TrashCan,
Edit,
// @ts-ignore
} from '@carbon/icons-react';
// @ts-ignore
import { Button, Table, Stack } from '@carbon/react';
import { Button, Stack } 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,
unModifyProcessModelPath,
modifyProcessIdentifierForPathParam,
unModifyProcessIdentifierForPathParam,
} from '../helpers';
import {
PaginationObject,
PermissionsToCheck,
ProcessGroup,
ProcessModel,
// ProcessModel,
} from '../interfaces';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { usePermissionFetcher } from '../hooks/PermissionService';
import ProcessGroupListTiles from '../components/ProcessGroupListTiles';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import ProcessModelListTiles from '../components/ProcessModelListTiles';
export default function ProcessGroupShow() {
const params = useParams();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [processGroup, setProcessGroup] = useState<ProcessGroup | null>(null);
const [processModels, setProcessModels] = useState([]);
// const [processModels, setProcessModels] = useState([]);
const [modelPagination, setModelPagination] =
useState<PaginationObject | null>(null);
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processGroupListPath]: ['POST'],
[targetUris.processGroupShowPath]: ['PUT'],
[targetUris.processGroupShowPath]: ['PUT', 'DELETE'],
[targetUris.processModelCreatePath]: ['POST'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
@ -42,12 +54,12 @@ export default function ProcessGroupShow() {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
const setProcessModelFromResult = (result: any) => {
setProcessModels(result.results);
// setProcessModels(result.results);
setModelPagination(result.pagination);
};
const processResult = (result: any) => {
setProcessGroup(result);
const unmodifiedProcessGroupId = unModifyProcessModelPath(
const unmodifiedProcessGroupId = unModifyProcessIdentifierForPathParam(
(params as any).process_group_id
);
HttpService.makeCallToBackend({
@ -61,56 +73,105 @@ export default function ProcessGroupShow() {
});
}, [params, searchParams]);
const buildModelTable = () => {
if (processGroup === null) {
return null;
// const buildModelTable = () => {
// if (processGroup === null) {
// return null;
// }
// const rows = processModels.map((row: ProcessModel) => {
// const modifiedProcessModelId: String =
// modifyProcessIdentifierForPathParam((row as any).id);
// return (
// <tr key={row.id}>
// <td>
// <Link
// to={`/admin/process-models/${modifiedProcessModelId}`}
// data-qa="process-model-show-link"
// >
// {row.id}
// </Link>
// </td>
// <td>{row.display_name}</td>
// </tr>
// );
// });
// return (
// <div>
// <h2>Process Models</h2>
// <Table striped bordered>
// <thead>
// <tr>
// <th>Process Model Id</th>
// <th>Display Name</th>
// </tr>
// </thead>
// <tbody>{rows}</tbody>
// </Table>
// </div>
// );
// };
const navigateToProcessGroups = (_result: any) => {
navigate(`/admin/process-groups`);
};
const deleteProcessGroup = () => {
if (processGroup) {
HttpService.makeCallToBackend({
path: `/process-groups/${modifyProcessIdentifierForPathParam(
processGroup.id
)}`,
successCallback: navigateToProcessGroups,
httpMethod: 'DELETE',
});
}
const rows = processModels.map((row: ProcessModel) => {
const modifiedProcessModelId: String = modifyProcessModelPath(
(row as any).id
);
return (
<tr key={row.id}>
<td>
<Link
to={`/admin/process-models/${modifiedProcessModelId}`}
data-qa="process-model-show-link"
>
{row.id}
</Link>
</td>
<td>{row.display_name}</td>
</tr>
);
});
return (
<div>
<h2>Process Models</h2>
<Table striped bordered>
<thead>
<tr>
<th>Process Model Id</th>
<th>Display Name</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
</div>
);
};
if (processGroup && modelPagination) {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
const modifiedProcessGroupId = modifyProcessModelPath(processGroup.id);
// const { page, perPage } = getPageInfoFromSearchParams(searchParams);
const modifiedProcessGroupId = modifyProcessIdentifierForPathParam(
processGroup.id
);
return (
<>
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
['', `process_group:${processGroup.id}`],
{
entityToExplode: processGroup,
entityType: 'process-group',
},
]}
/>
<h1>Process Group: {processGroup.display_name}</h1>
<Stack orientation="horizontal" gap={1}>
<h1 className="with-icons">
Process Group: {processGroup.display_name}
</h1>
<Can I="PUT" a={targetUris.processGroupShowPath} ability={ability}>
<Button
kind="ghost"
data-qa="edit-process-group-button"
renderIcon={Edit}
iconDescription="Edit Process Group"
hasIconOnly
href={`/admin/process-groups/${modifiedProcessGroupId}/edit`}
>
Edit process group
</Button>
</Can>
<Can I="DELETE" a={targetUris.processGroupShowPath} ability={ability}>
<ButtonWithConfirmation
kind="ghost"
data-qa="delete-process-group-button"
renderIcon={TrashCan}
iconDescription="Delete Process Group"
hasIconOnly
description={`Delete process group: ${processGroup.display_name}`}
onConfirmation={deleteProcessGroup}
confirmButtonLabel="Delete"
/>
</Can>
</Stack>
<p className="process-description">{processGroup.description}</p>
<ul>
<Stack orientation="horizontal" gap={3}>
<Can I="POST" a={targetUris.processGroupListPath} ability={ability}>
@ -131,30 +192,27 @@ export default function ProcessGroupShow() {
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 />
<ProcessModelListTiles
headerElement={<h2>Process Models</h2>}
processGroup={processGroup}
/>
{/* eslint-disable-next-line sonarjs/no-gratuitous-expressions */}
{modelPagination && modelPagination.total > 0 && (
{/* {modelPagination && modelPagination.total > 0 && (
<PaginationForTable
page={page}
perPage={perPage}
pagination={modelPagination}
tableToDisplay={buildModelTable()}
/>
)}
)} */}
<br />
<br />
<ProcessGroupListTiles
processGroup={processGroup}
headerElement={<h2>Process Groups</h2>}
headerElement={<h2 className="clear-left">Process Groups</h2>}
/>
</ul>
</>

View File

@ -1,25 +1,27 @@
import { useEffect, useState } from 'react';
// @ts-ignore
import { Table } from '@carbon/react';
import { Table, Tabs, TabList, Tab } from '@carbon/react';
import { useParams, useSearchParams, Link } from 'react-router-dom';
import PaginationForTable from '../components/PaginationForTable';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import {
getPageInfoFromSearchParams,
modifyProcessModelPath,
unModifyProcessModelPath,
modifyProcessIdentifierForPathParam,
convertSecondsToFormattedDateTime,
} from '../helpers';
import HttpService from '../services/HttpService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
export default function ProcessInstanceLogList() {
const params = useParams();
const [searchParams] = useSearchParams();
const [searchParams, setSearchParams] = useSearchParams();
const [processInstanceLogs, setProcessInstanceLogs] = useState([]);
const [pagination, setPagination] = useState(null);
const modifiedProcessModelId = modifyProcessModelPath(
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
`${params.process_model_id}`
);
const { targetUris } = useUriListForPermissions();
const isDetailedView = searchParams.get('detailed') === 'true';
useEffect(() => {
const setProcessInstanceLogListFromResult = (result: any) => {
@ -28,26 +30,36 @@ export default function ProcessInstanceLogList() {
};
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
HttpService.makeCallToBackend({
path: `/process-instances/${params.process_instance_id}/logs?per_page=${perPage}&page=${page}`,
path: `${targetUris.processInstanceLogListPath}?per_page=${perPage}&page=${page}&detailed=${isDetailedView}`,
successCallback: setProcessInstanceLogListFromResult,
});
}, [searchParams, params]);
}, [
searchParams,
params,
targetUris.processInstanceLogListPath,
isDetailedView,
]);
const buildTable = () => {
const rows = processInstanceLogs.map((row) => {
const rowToUse = row as any;
return (
<tr key={rowToUse.id}>
<td>{rowToUse.bpmn_process_identifier}</td>
<td>{rowToUse.id}</td>
<td>{rowToUse.message}</td>
<td>{rowToUse.bpmn_task_identifier}</td>
<td>{rowToUse.bpmn_task_name}</td>
<td>{rowToUse.bpmn_task_type}</td>
{isDetailedView && (
<>
<td>{rowToUse.bpmn_task_identifier}</td>
<td>{rowToUse.bpmn_task_type}</td>
<td>{rowToUse.bpmn_process_identifier}</td>
</>
)}
<td>{rowToUse.username}</td>
<td>
<Link
data-qa="process-instance-show-link"
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${rowToUse.process_instance_id}/${rowToUse.spiff_step}`}
to={`/admin/process-instances/${modifiedProcessModelId}/${rowToUse.process_instance_id}/${rowToUse.spiff_step}`}
>
{convertSecondsToFormattedDateTime(rowToUse.timestamp)}
</Link>
@ -59,11 +71,16 @@ export default function ProcessInstanceLogList() {
<Table size="lg">
<thead>
<tr>
<th>Bpmn Process Identifier</th>
<th>Id</th>
<th>Message</th>
<th>Task Identifier</th>
<th>Task Name</th>
<th>Task Type</th>
{isDetailedView && (
<>
<th>Task Identifier</th>
<th>Task Type</th>
<th>Bpmn Process Identifier</th>
</>
)}
<th>User</th>
<th>Timestamp</th>
</tr>
@ -72,34 +89,57 @@ export default function ProcessInstanceLogList() {
</Table>
);
};
const selectedTabIndex = isDetailedView ? 1 : 0;
if (pagination) {
const { page, perPage } = getPageInfoFromSearchParams(searchParams);
return (
<main>
<>
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Model: ${params.process_model_id}`,
`process_model:${unModifyProcessModelPath(
params.process_model_id || ''
)}:link`,
],
{
entityToExplode: params.process_model_id || '',
entityType: 'process-model-id',
linkLastItem: true,
},
[
`Process Instance: ${params.process_instance_id}`,
`/admin/process-models/${params.process_model_id}/process-instances/${params.process_instance_id}`,
`/admin/process-instances/${params.process_model_id}/${params.process_instance_id}`,
],
['Logs'],
]}
/>
<Tabs selectedIndex={selectedTabIndex}>
<TabList aria-label="List of tabs">
<Tab
title="Only show a subset of the logs, and show fewer columns"
onClick={() => {
searchParams.set('detailed', 'false');
setSearchParams(searchParams);
}}
>
Simple
</Tab>
<Tab
title="Show all logs for this process instance, and show extra columns that may be useful for debugging"
onClick={() => {
searchParams.set('detailed', 'true');
setSearchParams(searchParams);
}}
>
Detailed
</Tab>
</TabList>
</Tabs>
<br />
<PaginationForTable
page={page}
perPage={perPage}
pagination={pagination}
tableToDisplay={buildTable()}
/>
</main>
</>
);
}
return null;

View File

@ -2,12 +2,22 @@ import { useEffect, useState } from 'react';
// @ts-ignore
import { Button, Table } from '@carbon/react';
import { useParams, Link } from 'react-router-dom';
import { Can } from '@casl/react';
import HttpService from '../services/HttpService';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
export default function ProcessInstanceReportList() {
const params = useParams();
const [processInstanceReports, setProcessInstanceReports] = useState([]);
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processInstanceReportListPath]: ['POST'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
useEffect(() => {
HttpService.makeCallToBackend({
path: `/process-instances/reports`,
@ -45,9 +55,11 @@ export default function ProcessInstanceReportList() {
const headerStuff = (
<>
<h1>Process Instance Perspectives</h1>
<Button href="/admin/process-instances/reports/new">
Add a process instance perspective
</Button>
<Can I="POST" a={targetUris.processInstanceListPath} ability={ability}>
<Button href="/admin/process-instances/reports/new">
Add a process instance perspective
</Button>
</Can>
</>
);
if (processInstanceReports?.length > 0) {

View File

@ -76,9 +76,7 @@ export default function ProcessInstanceReport() {
return (
<main>
<ProcessBreadcrumb
processModelId={params.process_model_id}
processGroupId={params.process_group_id}
linkProcessModel
hotCrumbs={[['Process Groups', '/admin'], ['Process Instance']]}
/>
<h1>Process Instance Perspective: {params.report_identifier}</h1>
<Button

View File

@ -29,7 +29,7 @@ import HttpService from '../services/HttpService';
import ReactDiagramEditor from '../components/ReactDiagramEditor';
import {
convertSecondsToFormattedDateTime,
unModifyProcessModelPath,
unModifyProcessIdentifierForPathParam,
} from '../helpers';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import ErrorContext from '../contexts/ErrorContext';
@ -43,13 +43,14 @@ export default function ProcessInstanceShow() {
const [processInstance, setProcessInstance] = useState(null);
const [tasks, setTasks] = useState<Array<object> | null>(null);
const [tasksCallHadError, setTasksCallHadError] = useState<boolean>(false);
const [taskToDisplay, setTaskToDisplay] = useState<object | null>(null);
const [taskDataToDisplay, setTaskDataToDisplay] = useState<string>('');
const [editingTaskData, setEditingTaskData] = useState<boolean>(false);
const setErrorMessage = (useContext as any)(ErrorContext)[1];
const unModifiedProcessModelId = unModifyProcessModelPath(
const unModifiedProcessModelId = unModifyProcessIdentifierForPathParam(
`${params.process_model_id}`
);
const modifiedProcessModelId = params.process_model_id;
@ -57,8 +58,16 @@ export default function ProcessInstanceShow() {
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.messageInstanceListPath]: ['GET'],
[targetUris.processInstanceTaskListPath]: ['GET'],
[targetUris.processInstanceActionPath]: ['DELETE'],
[targetUris.processInstanceLogListPath]: ['GET'],
[`${targetUris.processInstanceActionPath}/suspend`]: ['PUT'],
[`${targetUris.processInstanceActionPath}/terminate`]: ['PUT'],
[`${targetUris.processInstanceActionPath}/resume`]: ['PUT'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
const { ability, permissionsLoaded } = usePermissionFetcher(
permissionRequestData
);
const navigateToProcessInstances = (_result: any) => {
navigate(
@ -67,25 +76,33 @@ export default function ProcessInstanceShow() {
};
useEffect(() => {
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}/process-instances/${params.process_instance_id}`,
successCallback: setProcessInstance,
});
if (typeof params.spiff_step === 'undefined')
if (permissionsLoaded) {
const processTaskFailure = () => {
setTasksCallHadError(true);
};
HttpService.makeCallToBackend({
path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}/tasks?all_tasks=true`,
successCallback: setTasks,
path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}`,
successCallback: setProcessInstance,
});
else
HttpService.makeCallToBackend({
path: `/process-instances/${modifiedProcessModelId}/${params.process_instance_id}/tasks?all_tasks=true&spiff_step=${params.spiff_step}`,
successCallback: setTasks,
});
}, [params, modifiedProcessModelId]);
let taskParams = '?all_tasks=true';
if (typeof params.spiff_step !== 'undefined') {
taskParams = `${taskParams}&spiff_step=${params.spiff_step}`;
}
if (ability.can('GET', targetUris.processInstanceTaskListPath)) {
HttpService.makeCallToBackend({
path: `${targetUris.processInstanceTaskListPath}${taskParams}`,
successCallback: setTasks,
failureCallback: processTaskFailure,
});
} else {
setTasksCallHadError(true);
}
}
}, [params, modifiedProcessModelId, permissionsLoaded, ability, targetUris]);
const deleteProcessInstance = () => {
HttpService.makeCallToBackend({
path: `/process-instances/${params.process_instance_id}`,
path: targetUris.processInstanceActionPath,
successCallback: navigateToProcessInstances,
httpMethod: 'DELETE',
});
@ -98,7 +115,7 @@ export default function ProcessInstanceShow() {
const terminateProcessInstance = () => {
HttpService.makeCallToBackend({
path: `/process-instances/${params.process_instance_id}/terminate`,
path: `${targetUris.processInstanceActionPath}/terminate`,
successCallback: refreshPage,
httpMethod: 'POST',
});
@ -106,7 +123,7 @@ export default function ProcessInstanceShow() {
const suspendProcessInstance = () => {
HttpService.makeCallToBackend({
path: `/process-instances/${params.process_instance_id}/suspend`,
path: `${targetUris.processInstanceActionPath}/suspend`,
successCallback: refreshPage,
httpMethod: 'POST',
});
@ -114,7 +131,7 @@ export default function ProcessInstanceShow() {
const resumeProcessInstance = () => {
HttpService.makeCallToBackend({
path: `/process-instances/${params.process_instance_id}/resume`,
path: `${targetUris.processInstanceActionPath}/resume`,
successCallback: refreshPage,
httpMethod: 'POST',
});
@ -162,7 +179,7 @@ export default function ProcessInstanceShow() {
<Link
reloadDocument
data-qa="process-instance-step-link"
to={`/admin/process-models/${
to={`/admin/process-instances/${
params.process_model_id
}/process-instances/${params.process_instance_id}/${
currentSpiffStep(processInstanceToUse) + distance
@ -197,7 +214,7 @@ export default function ProcessInstanceShow() {
if (currentEndDate) {
currentEndDateTag = (
<Grid condensed fullWidth>
<Column sm={1} md={1} lg={1} className="grid-list-title">
<Column sm={1} md={1} lg={2} className="grid-list-title">
Completed:{' '}
</Column>
<Column sm={3} md={3} lg={3} className="grid-date">
@ -223,7 +240,7 @@ export default function ProcessInstanceShow() {
return (
<>
<Grid condensed fullWidth>
<Column sm={1} md={1} lg={1} className="grid-list-title">
<Column sm={1} md={1} lg={2} className="grid-list-title">
Started:{' '}
</Column>
<Column sm={3} md={3} lg={3} className="grid-date">
@ -234,7 +251,7 @@ export default function ProcessInstanceShow() {
</Grid>
{currentEndDateTag}
<Grid condensed fullWidth>
<Column sm={1} md={1} lg={1} className="grid-list-title">
<Column sm={1} md={1} lg={2} className="grid-list-title">
Status:{' '}
</Column>
<Column sm={3} md={3} lg={3}>
@ -247,14 +264,20 @@ export default function ProcessInstanceShow() {
<Grid condensed fullWidth>
<Column sm={2} md={2} lg={2}>
<ButtonSet>
<Button
size="sm"
className="button-white-background"
data-qa="process-instance-log-list-link"
href={`/admin/process-models/${modifiedProcessModelId}/process-instances/${params.process_instance_id}/logs`}
<Can
I="GET"
a={targetUris.processInstanceLogListPath}
ability={ability}
>
Logs
</Button>
<Button
size="sm"
className="button-white-background"
data-qa="process-instance-log-list-link"
href={`/admin/logs/${modifiedProcessModelId}/${params.process_instance_id}`}
>
Logs
</Button>
</Can>
<Can
I="GET"
a={targetUris.messageInstanceListPath}
@ -424,8 +447,8 @@ export default function ProcessInstanceShow() {
// taskToUse is copy of taskToDisplay, with taskDataToDisplay in data attribute
const taskToUse: any = { ...taskToDisplay, data: taskDataToDisplay };
HttpService.makeCallToBackend({
path: `/process-instances/${params.process_instance_id}/task/${taskToUse.id}/update`,
httpMethod: 'POST',
path: `/task-data/${modifiedProcessModelId}/${params.process_instance_id}/${taskToUse.id}`,
httpMethod: 'PUT',
successCallback: saveTaskDataResult,
failureCallback: saveTaskDataFailure,
postBody: {
@ -532,28 +555,40 @@ export default function ProcessInstanceShow() {
const buttonIcons = (processInstanceToUse: any) => {
const elements = [];
elements.push(terminateButton(processInstanceToUse));
elements.push(suspendButton(processInstanceToUse));
elements.push(resumeButton(processInstanceToUse));
elements.push(
<ButtonWithConfirmation
data-qa="process-instance-delete"
kind="ghost"
renderIcon={TrashCan}
iconDescription="Delete"
hasIconOnly
description={`Delete Process Instance: ${processInstanceToUse.id}`}
onConfirmation={deleteProcessInstance}
confirmButtonLabel="Delete"
/>
);
if (
ability.can('POST', `${targetUris.processInstanceActionPath}/terminate`)
) {
elements.push(terminateButton(processInstanceToUse));
}
if (
ability.can('POST', `${targetUris.processInstanceActionPath}/suspend`)
) {
elements.push(suspendButton(processInstanceToUse));
}
if (ability.can('POST', `${targetUris.processInstanceActionPath}/resume`)) {
elements.push(resumeButton(processInstanceToUse));
}
if (ability.can('DELETE', targetUris.processInstanceActionPath)) {
elements.push(
<ButtonWithConfirmation
data-qa="process-instance-delete"
kind="ghost"
renderIcon={TrashCan}
iconDescription="Delete"
hasIconOnly
description={`Delete Process Instance: ${processInstanceToUse.id}`}
onConfirmation={deleteProcessInstance}
confirmButtonLabel="Delete"
/>
);
}
return elements;
};
if (processInstance && tasks) {
if (processInstance && (tasks || tasksCallHadError)) {
const processInstanceToUse = processInstance as any;
const taskIds = getTaskIds();
const processModelId = unModifyProcessModelPath(
const processModelId = unModifyProcessIdentifierForPathParam(
params.process_model_id ? params.process_model_id : ''
);
@ -562,10 +597,11 @@ export default function ProcessInstanceShow() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Model: ${processModelId}`,
`process_model:${processModelId}:link`,
],
{
entityToExplode: processModelId,
entityType: 'process-model-id',
linkLastItem: true,
},
[`Process Instance Id: ${processInstanceToUse.id}`],
]}
/>

View File

@ -24,10 +24,11 @@ export default function ProcessModelEdit() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Model: ${processModel.id}`,
`process_model:${processModel.id}:link`,
],
{
entityToExplode: processModel,
entityType: 'process-model',
linkLastItem: true,
},
]}
/>
<h1>Edit Process Model: {(processModel as any).id}</h1>

View File

@ -17,7 +17,7 @@ import ReactDiagramEditor from '../components/ReactDiagramEditor';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import HttpService from '../services/HttpService';
import ErrorContext from '../contexts/ErrorContext';
import { makeid, modifyProcessModelPath } from '../helpers';
import { makeid, modifyProcessIdentifierForPathParam } from '../helpers';
import {
CarbonComboBoxProcessSelection,
ProcessFile,
@ -94,7 +94,7 @@ export default function ProcessModelEditDiagram() {
const [bpmnXmlForDiagramRendering, setBpmnXmlForDiagramRendering] =
useState(null);
const modifiedProcessModelId = modifyProcessModelPath(
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
(params as any).process_model_id
);
@ -283,7 +283,7 @@ export default function ProcessModelEditDiagram() {
const onServiceTasksRequested = (event: any) => {
HttpService.makeCallToBackend({
path: `/service_tasks`,
path: `/service-tasks`,
successCallback: makeApiHandler(event),
});
};
@ -735,7 +735,7 @@ export default function ProcessModelEditDiagram() {
if (processModel) {
const files = processModel.files.filter((f) => f.type === type);
files.some((file) => {
if (file.references.some((ref) => ref.id === id)) {
if (file.references.some((ref) => ref.identifier === id)) {
matchFile = file;
return true;
}
@ -753,7 +753,7 @@ export default function ProcessModelEditDiagram() {
const path = generatePath(
'/admin/process-models/:process_model_path/files/:file_name',
{
process_model_path: modifyProcessModelPath(
process_model_path: modifyProcessIdentifierForPathParam(
processRef.process_model_id
),
file_name: processRef.file_name,
@ -844,10 +844,11 @@ export default function ProcessModelEditDiagram() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Model: ${processModel.id}`,
`process_model:${processModel.id}:link`,
],
{
entityToExplode: processModel,
entityType: 'process-model',
linkLastItem: true,
},
[processModelFileName],
]}
/>

View File

@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import { ProcessModel } from '../interfaces';
import ProcessModelForm from '../components/ProcessModelForm';
import { unModifyProcessIdentifierForPathParam } from '../helpers';
export default function ProcessModelNew() {
const params = useParams();
@ -19,16 +20,19 @@ export default function ProcessModelNew() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Group: ${params.process_group_id}`,
`process_group:${params.process_group_id}:link`,
],
{
entityToExplode: params.process_group_id || '',
entityType: 'process-group-id',
linkLastItem: true,
},
]}
/>
<h1>Add Process Model</h1>
<ProcessModelForm
mode="new"
processGroupId={params.process_group_id}
processGroupId={unModifyProcessIdentifierForPathParam(
params.process_group_id || ''
)}
processModel={processModel}
setProcessModel={setProcessModel}
/>

View File

@ -7,6 +7,8 @@ import {
TrashCan,
Favorite,
Edit,
View,
ArrowRight,
// @ts-ignore
} from '@carbon/icons-react';
import {
@ -33,69 +35,20 @@ import HttpService from '../services/HttpService';
import ErrorContext from '../contexts/ErrorContext';
import {
getGroupFromModifiedModelId,
modifyProcessModelPath,
modifyProcessIdentifierForPathParam,
} 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
) => {
// All values stored in localStorage are strings.
// Grab our recentProcessModels string from localStorage.
const stringFromLocalStorage = window.localStorage.getItem(
'recentProcessModels'
);
// adapted from https://stackoverflow.com/a/59424458/6090676
// If that value is null (meaning that we've never saved anything to that spot in localStorage before), use an empty array as our array. Otherwise, use the value we parse out.
let array: RecentProcessModel[] = [];
if (stringFromLocalStorage !== null) {
// Then parse that string into an actual value.
array = JSON.parse(stringFromLocalStorage);
}
// Here's the value we want to add
const value = {
processModelIdentifier: processModelForStorage.id,
processModelDisplayName: processModelForStorage.display_name,
};
// anything with a processGroupIdentifier is old and busted. leave it behind.
array = array.filter((item) => item.processGroupIdentifier === undefined);
// If our parsed/empty array doesn't already have this value in it...
const matchingItem = array.find(
(item) => item.processModelIdentifier === value.processModelIdentifier
);
if (matchingItem === undefined) {
// add the value to the beginning of the array
array.unshift(value);
// Keep the array to 3 items
if (array.length > 3) {
array.pop();
}
}
// once the old and busted serializations are gone, we can put these two statements inside the above if statement
// turn the array WITH THE NEW VALUE IN IT into a string to prepare it to be stored in localStorage
const stringRepresentingArray = JSON.stringify(array);
// and store it in localStorage as "recentProcessModels"
window.localStorage.setItem('recentProcessModels', stringRepresentingArray);
};
import { Notification } from '../components/Notification';
export default function ProcessModelShow() {
const params = useParams();
@ -108,18 +61,23 @@ export default function ProcessModelShow() {
const [filesToUpload, setFilesToUpload] = useState<any>(null);
const [showFileUploadModal, setShowFileUploadModal] =
useState<boolean>(false);
const [processModelPublished, setProcessModelPublished] = useState<any>(null);
const [publishDisabled, setPublishDisabled] = useState<boolean>(false);
const navigate = useNavigate();
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processModelShowPath]: ['PUT', 'DELETE'],
[targetUris.processModelPublishPath]: ['POST'],
[targetUris.processInstanceListPath]: ['GET'],
[targetUris.processInstanceActionPath]: ['POST'],
[targetUris.processModelFileCreatePath]: ['POST', 'GET', 'DELETE'],
[targetUris.processInstanceCreatePath]: ['POST'],
[targetUris.processModelFileCreatePath]: ['POST', 'PUT', 'GET', 'DELETE'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
const { ability, permissionsLoaded } = usePermissionFetcher(
permissionRequestData
);
const modifiedProcessModelId = modifyProcessModelPath(
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
`${params.process_model_id}`
);
@ -127,7 +85,6 @@ export default function ProcessModelShow() {
const processResult = (result: ProcessModel) => {
setProcessModel(result);
setReloadModel(false);
storeRecentProcessModelInLocalStorage(result);
};
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}`,
@ -138,18 +95,17 @@ export default function ProcessModelShow() {
const processInstanceRunResultTag = () => {
if (processInstance) {
return (
<div className="alert alert-success" role="alert">
<p>
Process Instance {processInstance.id} kicked off (
<Link
to={`/admin/process-models/${modifiedProcessModelId}/process-instances/${processInstance.id}`}
data-qa="process-instance-show-link"
>
view
</Link>
).
</p>
</div>
<Notification
title="Process Instance Kicked Off:"
onClose={() => setProcessInstance(null)}
>
<Link
to={`/admin/process-instances/${modifiedProcessModelId}/${processInstance.id}`}
data-qa="process-instance-show-link"
>
view
</Link>
</Notification>
);
}
return null;
@ -249,6 +205,21 @@ export default function ProcessModelShow() {
});
};
const postPublish = (value: any) => {
setPublishDisabled(false);
setProcessModelPublished(value);
};
const publishProcessModel = () => {
setPublishDisabled(true);
setProcessModelPublished(null);
HttpService.makeCallToBackend({
path: `/process-models/${modifiedProcessModelId}/publish`,
successCallback: postPublish,
httpMethod: 'POST',
});
};
const navigateToFileEdit = (processModelFile: ProcessFile) => {
const url = profileModelFileEditUrl(processModelFile);
if (url) {
@ -261,12 +232,18 @@ export default function ProcessModelShow() {
isPrimaryBpmnFile: boolean
) => {
const elements = [];
let icon = View;
let actionWord = 'View';
if (ability.can('PUT', targetUris.processModelFileCreatePath)) {
icon = Edit;
actionWord = 'Edit';
}
elements.push(
<Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
<Button
kind="ghost"
renderIcon={Edit}
iconDescription="Edit File"
renderIcon={icon}
iconDescription={`${actionWord} File`}
hasIconOnly
size="lg"
data-qa={`edit-file-${processModelFile.name.replace('.', '-')}`}
@ -324,7 +301,7 @@ export default function ProcessModelShow() {
};
const processModelFileList = () => {
if (!processModel) {
if (!processModel || !permissionsLoaded) {
return null;
}
let constructedTag;
@ -438,12 +415,16 @@ export default function ProcessModelShow() {
);
};
const processModelButtons = () => {
const processModelFilesSection = () => {
if (!processModel) {
return null;
}
return (
<Grid condensed fullWidth>
<Grid
condensed
fullWidth
className="megacondensed process-model-files-section"
>
<Column md={5} lg={9} sm={3}>
<Accordion align="end" open>
<AccordionItem
@ -514,6 +495,55 @@ export default function ProcessModelShow() {
);
};
const processInstanceListTableButton = () => {
if (processModel) {
return (
<Grid fullWidth condensed>
<Column sm={{ span: 3 }} md={{ span: 4 }} lg={{ span: 3 }}>
<h2>Process Instances</h2>
</Column>
<Column
sm={{ span: 1, offset: 3 }}
md={{ span: 1, offset: 7 }}
lg={{ span: 1, offset: 15 }}
>
<Button
data-qa="process-instance-list-link"
kind="ghost"
renderIcon={ArrowRight}
iconDescription="Go to Filterable List"
hasIconOnly
size="lg"
onClick={() =>
navigate(
`/admin/process-instances?process_model_identifier=${processModel.id}`
)
}
/>
</Column>
</Grid>
);
}
return null;
};
const processModelPublishMessage = () => {
if (processModelPublished) {
const prUrl: string = processModelPublished.pr_url;
return (
<Notification
title="Model Published:"
onClose={() => setProcessModelPublished(false)}
>
<a href={prUrl} target="_void()">
View the changes and create a Pull Request
</a>
</Notification>
);
}
return null;
};
if (processModel) {
return (
<>
@ -521,20 +551,34 @@ export default function ProcessModelShow() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Model: ${processModel.id}`,
`process_model:${processModel.id}`,
],
{
entityToExplode: processModel,
entityType: 'process-model',
},
]}
/>
{processModelPublishMessage()}
{processInstanceRunResultTag()}
<Stack orientation="horizontal" gap={1}>
<h1 className="with-icons">
Process Model: {processModel.display_name}
</h1>
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
<Button
kind="ghost"
data-qa="edit-process-model-button"
renderIcon={Edit}
iconDescription="Edit Process Model"
hasIconOnly
href={`/admin/process-models/${modifiedProcessModelId}/edit`}
>
Edit process model
</Button>
</Can>
<Can I="DELETE" a={targetUris.processModelShowPath} ability={ability}>
<ButtonWithConfirmation
kind="ghost"
data-qa="delete-process-model-button"
renderIcon={TrashCan}
iconDescription="Delete Process Model"
hasIconOnly
@ -548,36 +592,38 @@ export default function ProcessModelShow() {
<Stack orientation="horizontal" gap={3}>
<Can
I="POST"
a={targetUris.processInstanceActionPath}
a={targetUris.processInstanceCreatePath}
ability={ability}
>
<ProcessInstanceRun
processModel={processModel}
onSuccessCallback={setProcessInstance}
/>
<>
<ProcessInstanceRun
processModel={processModel}
onSuccessCallback={setProcessInstance}
/>
<br />
<br />
</>
</Can>
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
<Button
href={`/admin/process-models/${modifiedProcessModelId}/edit`}
variant="secondary"
>
Edit process model
<Can
I="POST"
a={targetUris.processModelPublishPath}
ability={ability}
>
<Button disabled={publishDisabled} onClick={publishProcessModel}>
Publish Changes
</Button>
</Can>
</Stack>
<br />
<br />
{processInstanceRunResultTag()}
<br />
{processModelFilesSection()}
<Can I="GET" a={targetUris.processInstanceListPath} ability={ability}>
{processInstanceListTableButton()}
<ProcessInstanceListTable
filtersEnabled={false}
processModelFullIdentifier={processModel.id}
perPageOptions={[2, 5, 25]}
showReports={false}
/>
<br />
</Can>
{processModelButtons()}
</>
);
}

View File

@ -6,7 +6,7 @@ import { Button, Modal } from '@carbon/react';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import HttpService from '../services/HttpService';
import ButtonWithConfirmation from '../components/ButtonWithConfirmation';
import { modifyProcessModelPath, unModifyProcessModelPath } from '../helpers';
import { modifyProcessIdentifierForPathParam } from '../helpers';
import { ProcessFile } from '../interfaces';
// NOTE: This is mostly the same as ProcessModelEditDiagram and if we go this route could
@ -38,7 +38,7 @@ export default function ReactFormEditor() {
const editorDefaultLanguage = fileExtension === 'md' ? 'markdown' : 'json';
const modifiedProcessModelId = modifyProcessModelPath(
const modifiedProcessModelId = modifyProcessIdentifierForPathParam(
`${params.process_model_id}`
);
@ -156,14 +156,11 @@ export default function ReactFormEditor() {
<ProcessBreadcrumb
hotCrumbs={[
['Process Groups', '/admin'],
[
`Process Model: ${unModifyProcessModelPath(
params.process_model_id || ''
)}`,
`process_model:${unModifyProcessModelPath(
params.process_model_id || ''
)}:link`,
],
{
entityToExplode: params.process_model_id || '',
entityType: 'process-model-id',
linkLastItem: true,
},
[processModelFileName],
]}
/>
@ -190,6 +187,7 @@ export default function ReactFormEditor() {
)}
{params.file_name ? (
<ButtonWithConfirmation
data-qa="delete-process-model-file"
description={`Delete file ${params.file_name}?`}
onConfirmation={deleteFile}
buttonLabel="Delete"

View File

@ -62,7 +62,7 @@ export default function SecretList() {
<Table striped bordered>
<thead>
<tr>
<th>ID</th>
<th>Id</th>
<th>Secret Key</th>
<th>Creator</th>
<th>Delete</th>

View File

@ -15,6 +15,7 @@ import {
Tabs,
Grid,
Column,
Button,
// @ts-ignore
} from '@carbon/react';
@ -24,7 +25,10 @@ import remarkGfm from 'remark-gfm';
import Form from '../themes/carbon';
import HttpService from '../services/HttpService';
import ErrorContext from '../contexts/ErrorContext';
import { modifyProcessModelPath } from '../helpers';
import { modifyProcessIdentifierForPathParam } from '../helpers';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
export default function TaskShow() {
const [task, setTask] = useState(null);
@ -34,24 +38,36 @@ 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,
});
};
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processInstanceTaskListPath]: ['GET'],
};
const { ability, permissionsLoaded } = usePermissionFetcher(
permissionRequestData
);
HttpService.makeCallToBackend({
path: `/tasks/${params.process_instance_id}/${params.task_id}`,
successCallback: processResult,
// This causes the page to continuously reload
// failureCallback: setErrorMessage,
});
}, [params]);
useEffect(() => {
if (permissionsLoaded) {
const processResult = (result: any) => {
setTask(result);
if (ability.can('GET', targetUris.processInstanceTaskListPath)) {
HttpService.makeCallToBackend({
path: `/task-data/${modifyProcessIdentifierForPathParam(
result.process_model_identifier
)}/${params.process_instance_id}`,
successCallback: setUserTasks,
});
}
};
HttpService.makeCallToBackend({
path: `/tasks/${params.process_instance_id}/${params.task_id}`,
successCallback: processResult,
// This causes the page to continuously reload
// failureCallback: setErrorMessage,
});
}
}, [params, permissionsLoaded, ability, targetUris]);
const processSubmitResult = (result: any) => {
setErrorMessage(null);
@ -115,17 +131,18 @@ export default function TaskShow() {
}
return null;
});
return (
<Tabs
title="Steps in this process instance involving people"
selectedIndex={selectedTabIndex}
>
<TabList aria-label="List of tabs" contained>
{userTasksElement}
</TabList>
</Tabs>
);
}
return (
<Tabs
title="Steps in this process instance involving people"
selectedIndex={selectedTabIndex}
>
<TabList aria-label="List of tabs" contained>
{userTasksElement}
</TabList>
</Tabs>
);
return null;
};
const formElement = (taskToUse: any) => {
@ -167,6 +184,14 @@ export default function TaskShow() {
reactFragmentToHideSubmitButton = <div />;
}
if (taskToUse.type === 'Manual Task') {
reactFragmentToHideSubmitButton = (
<div>
<Button type="submit">Continue</Button>
</div>
);
}
return (
<Grid fullWidth condensed>
<Column md={5} lg={8} sm={4}>
@ -198,7 +223,7 @@ export default function TaskShow() {
);
};
if (task && userTasks) {
if (task) {
const taskToUse = task as any;
let statusString = '';
if (taskToUse.state !== 'READY') {

View File

@ -66,9 +66,11 @@ backendCallProps) => {
method: httpMethod,
});
const updatedPath = path.replace(/^\/v1\.0/, '');
let isSuccessful = true;
let is403 = false;
fetch(`${BACKEND_BASE_URL}${path}`, httpArgs)
fetch(`${BACKEND_BASE_URL}${updatedPath}`, httpArgs)
.then((response) => {
if (response.status === 401) {
UserService.doLogin();