Merge pull request #40 from sartography/feature/model_show_permi_page

Feature/model show permi page
This commit is contained in:
jasquat 2022-11-15 17:39:02 -05:00 committed by GitHub
commit 36520c1c46
16 changed files with 205 additions and 153 deletions

View File

@ -258,7 +258,6 @@ paths:
description: The number of models to show per page. Defaults to page 10. description: The number of models to show per page. Defaults to page 10.
schema: schema:
type: integer type: integer
# process_model_list
get: get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_list operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_list
summary: Return a list of process models for a given process group summary: Return a list of process models for a given process group
@ -273,9 +272,10 @@ paths:
type: array type: array
items: items:
$ref: "#/components/schemas/ProcessModel" $ref: "#/components/schemas/ProcessModel"
# process_model_add
/process-models/{modified_process_group_id}:
post: post:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_add operationId: spiffworkflow_backend.routes.process_api_blueprint.process_model_create
summary: Creates a new process model with the given parameters. summary: Creates a new process model with the given parameters.
tags: tags:
- Process Models - Process Models
@ -371,7 +371,7 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/OkTrue" $ref: "#/components/schemas/OkTrue"
# process_model_list
/processes: /processes:
get: get:
operationId: spiffworkflow_backend.routes.process_api_blueprint.process_list operationId: spiffworkflow_backend.routes.process_api_blueprint.process_list

View File

@ -232,10 +232,10 @@ def process_group_show(
return make_response(jsonify(process_group), 200) return make_response(jsonify(process_group), 200)
def process_model_add( def process_model_create(
body: Dict[str, Union[str, bool, int]] body: Dict[str, Union[str, bool, int]]
) -> flask.wrappers.Response: ) -> flask.wrappers.Response:
"""Add_process_model.""" """Process_model_create."""
process_model_info = ProcessModelInfoSchema().load(body) process_model_info = ProcessModelInfoSchema().load(body)
if process_model_info is None: if process_model_info is None:
raise ApiError( raise ApiError(

View File

@ -701,11 +701,11 @@ class ProcessInstanceProcessor:
"bpmn_file_full_path_from_bpmn_process_identifier: bpmn_process_identifier is unexpectedly None" "bpmn_file_full_path_from_bpmn_process_identifier: bpmn_process_identifier is unexpectedly None"
) )
spec_reference = SpecReferenceCache.query.filter_by( spec_reference = (
identifier=bpmn_process_identifier SpecReferenceCache.query.filter_by(identifier=bpmn_process_identifier)
).filter_by( .filter_by(type="process")
type='process' .first()
).first() )
bpmn_file_full_path = None bpmn_file_full_path = None
if spec_reference is None: if spec_reference is None:
bpmn_file_full_path = ( bpmn_file_full_path = (
@ -1021,7 +1021,7 @@ class ProcessInstanceProcessor:
spiff_logger = logging.getLogger("spiff") spiff_logger = logging.getLogger("spiff")
for handler in spiff_logger.handlers: for handler in spiff_logger.handlers:
if hasattr(handler, "bulk_insert_logs"): if hasattr(handler, "bulk_insert_logs"):
handler.bulk_insert_logs() # type: ignoreidentifier handler.bulk_insert_logs() # type: ignore
db.session.commit() db.session.commit()
except WorkflowTaskExecException as we: except WorkflowTaskExecException as we:

View File

@ -240,11 +240,13 @@ class SpecFileService(FileSystemService):
SpecFileService.update_correlation_cache(ref) SpecFileService.update_correlation_cache(ref)
@staticmethod @staticmethod
def clear_caches_for_file(file_name: str, process_model_info: ProcessModelInfo) -> None: def clear_caches_for_file(
"""Clear all caches related to a file""" file_name: str, process_model_info: ProcessModelInfo
db.session.query(SpecReferenceCache).\ ) -> None:
filter(SpecReferenceCache.file_name == file_name).\ """Clear all caches related to a file."""
filter(SpecReferenceCache.process_model_id == process_model_info.id).delete() db.session.query(SpecReferenceCache).filter(
SpecReferenceCache.file_name == file_name
).filter(SpecReferenceCache.process_model_id == process_model_info.id).delete()
# fixme: likely the other caches should be cleared as well, but we don't have a clean way to do so yet. # fixme: likely the other caches should be cleared as well, but we don't have a clean way to do so yet.
@staticmethod @staticmethod

View File

@ -143,7 +143,6 @@ class TestNestedGroups(BaseTest):
response = client.get( # noqa: F841 response = client.get( # noqa: F841
target_uri, headers=self.logged_in_headers(user) target_uri, headers=self.logged_in_headers(user)
) )
print("test_nested_groups")
def test_add_nested_group( def test_add_nested_group(
self, self,
@ -153,10 +152,6 @@ class TestNestedGroups(BaseTest):
with_super_admin_user: UserModel, with_super_admin_user: UserModel,
) -> None: ) -> None:
"""Test_add_nested_group.""" """Test_add_nested_group."""
# user = self.find_or_create_user()
# self.add_permissions_to_user(
# user, target_uri=target_uri, permission_names=["read", "create"]
# )
process_group_a = ProcessGroup( process_group_a = ProcessGroup(
id="group_a", id="group_a",
display_name="Group A", display_name="Group A",
@ -194,16 +189,14 @@ class TestNestedGroups(BaseTest):
data=json.dumps(ProcessGroupSchema().dump(process_group_c)), data=json.dumps(ProcessGroupSchema().dump(process_group_c)),
) )
print("test_add_nested_group") def test_process_model_create(
def test_process_model_add(
self, self,
app: Flask, app: Flask,
client: FlaskClient, client: FlaskClient,
with_db_and_bpmn_file_cleanup: None, with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel, with_super_admin_user: UserModel,
) -> None: ) -> None:
"""Test_process_model_add.""" """Test_process_model_create."""
process_group_a = ProcessGroup( process_group_a = ProcessGroup(
id="group_a", id="group_a",
display_name="Group A", display_name="Group A",
@ -242,7 +235,6 @@ class TestNestedGroups(BaseTest):
content_type="application/json", content_type="application/json",
data=json.dumps(ProcessModelInfoSchema().dump(process_model)), data=json.dumps(ProcessModelInfoSchema().dump(process_model)),
) )
print("test_process_model_add")
def test_process_group_show( def test_process_group_show(
self, self,

View File

@ -105,14 +105,14 @@ class TestProcessApi(BaseTest):
assert response.json is not None assert response.json is not None
assert response.json == expected_response_body assert response.json == expected_response_body
def test_process_model_add( def test_process_model_create(
self, self,
app: Flask, app: Flask,
client: FlaskClient, client: FlaskClient,
with_db_and_bpmn_file_cleanup: None, with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel, with_super_admin_user: UserModel,
) -> None: ) -> None:
"""Test_add_new_process_model.""" """Test_process_model_create."""
process_group_id = "test_process_group" process_group_id = "test_process_group"
process_group_display_name = "Test Process Group" process_group_display_name = "Test Process Group"
# creates the group directory, and the json file # creates the group directory, and the json file

View File

@ -119,22 +119,21 @@ class TestSpecFileService(BaseTest):
== self.call_activity_nested_relative_file_path == self.call_activity_nested_relative_file_path
) )
def test_change_the_identifier_cleans_up_cache ( def test_change_the_identifier_cleans_up_cache(
self, self,
app: Flask, app: Flask,
client: FlaskClient, client: FlaskClient,
with_db_and_bpmn_file_cleanup: None, with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel, with_super_admin_user: UserModel,
) -> None: ) -> None:
""" When a BPMN processes identifier is changed in a file, the old id """When a BPMN processes identifier is changed in a file, the old id is removed from the cache."""
is removed from the cache"""
old_identifier = "ye_old_identifier" old_identifier = "ye_old_identifier"
process_id_lookup = SpecReferenceCache( process_id_lookup = SpecReferenceCache(
identifier=old_identifier, identifier=old_identifier,
relative_path=self.call_activity_nested_relative_file_path, relative_path=self.call_activity_nested_relative_file_path,
file_name=self.bpmn_file_name, file_name=self.bpmn_file_name,
process_model_id=f"{self.process_group_id}/{self.process_model_id}", process_model_id=f"{self.process_group_id}/{self.process_model_id}",
type='process' type="process",
) )
db.session.add(process_id_lookup) db.session.add(process_id_lookup)
db.session.commit() db.session.commit()
@ -157,7 +156,6 @@ class TestSpecFileService(BaseTest):
== self.call_activity_nested_relative_file_path == self.call_activity_nested_relative_file_path
) )
def test_load_reference_information( def test_load_reference_information(
self, self,
app: Flask, app: Flask,

View File

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

View File

@ -24,7 +24,7 @@ export default function App() {
[errorMessage] [errorMessage]
); );
const ability = defineAbility((can: any) => {}); const ability = defineAbility(() => {});
let errorTag = null; let errorTag = null;
if (errorMessage) { if (errorMessage) {

View File

@ -74,10 +74,7 @@ export default function ProcessModelForm({
if (hasErrors) { if (hasErrors) {
return; return;
} }
let path = `/process-models`; const path = `/process-models/${modifiedProcessModelPath}`;
if (mode === 'edit') {
path = `/process-models/${modifiedProcessModelPath}`;
}
let httpMethod = 'POST'; let httpMethod = 'POST';
if (mode === 'edit') { if (mode === 'edit') {
httpMethod = 'PUT'; httpMethod = 'PUT';

View File

@ -52,10 +52,14 @@ import TouchModule from 'diagram-js/lib/navigation/touch';
// @ts-expect-error TS(7016) FIXME // @ts-expect-error TS(7016) FIXME
import ZoomScrollModule from 'diagram-js/lib/navigation/zoomscroll'; import ZoomScrollModule from 'diagram-js/lib/navigation/zoomscroll';
import { Can } from '@casl/react';
import HttpService from '../services/HttpService'; import HttpService from '../services/HttpService';
import ButtonWithConfirmation from './ButtonWithConfirmation'; import ButtonWithConfirmation from './ButtonWithConfirmation';
import { makeid } from '../helpers'; import { makeid } from '../helpers';
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
import { PermissionsToCheck } from '../interfaces';
import { usePermissionFetcher } from '../hooks/PermissionService';
type OwnProps = { type OwnProps = {
processModelId: string; processModelId: string;
@ -107,6 +111,13 @@ export default function ReactDiagramEditor({
const alreadyImportedXmlRef = useRef(false); const alreadyImportedXmlRef = useRef(false);
const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = {
[targetUris.processModelShowPath]: ['PUT'],
[targetUris.processModelFileShowPath]: ['POST', 'GET', 'PUT', 'DELETE'],
};
const { ability } = usePermissionFetcher(permissionRequestData);
useEffect(() => { useEffect(() => {
if (diagramModelerState) { if (diagramModelerState) {
return; return;
@ -517,20 +528,40 @@ export default function ReactDiagramEditor({
if (diagramType !== 'readonly') { if (diagramType !== 'readonly') {
return ( return (
<> <>
<Button onClick={handleSave} variant="danger"> <Can
Save I="PUT"
</Button> a={targetUris.processModelFileShowPath}
{fileName && ( ability={ability}
<ButtonWithConfirmation >
description={`Delete file ${fileName}?`} <Button onClick={handleSave}>Save</Button>
onConfirmation={handleDelete} </Can>
buttonLabel="Delete" <Can
/> I="DELETE"
)} a={targetUris.processModelFileShowPath}
{onSetPrimaryFile && ( ability={ability}
<Button onClick={handleSetPrimaryFile}>Set as primary file</Button> >
)} {fileName && (
<Button onClick={downloadXmlFile}>Download xml</Button> <ButtonWithConfirmation
description={`Delete file ${fileName}?`}
onConfirmation={handleDelete}
buttonLabel="Delete"
/>
)}
</Can>
<Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
{onSetPrimaryFile && (
<Button onClick={handleSetPrimaryFile}>
Set as primary file
</Button>
)}
</Can>
<Can
I="GET"
a={targetUris.processModelFileShowPath}
ability={ability}
>
<Button onClick={downloadXmlFile}>Download xml</Button>
</Can>
</> </>
); );
} }

View File

@ -1,5 +1,5 @@
import { createContext } from 'react'; import { createContext } from 'react';
import { AbilityBuilder, Ability } from '@casl/ability'; import { Ability } from '@casl/ability';
import { createContextualCan } from '@casl/react'; import { createContextualCan } from '@casl/react';
export const AbilityContext = createContext(new Ability()); export const AbilityContext = createContext(new Ability());

View File

@ -12,19 +12,17 @@ export const usePermissionFetcher = (
useEffect(() => { useEffect(() => {
const processPermissionResult = (result: PermissionCheckResponseBody) => { const processPermissionResult = (result: PermissionCheckResponseBody) => {
const { can, cannot, rules } = new AbilityBuilder(Ability); const { can, cannot, rules } = new AbilityBuilder(Ability);
for (const [url, permissionVerbResults] of Object.entries( Object.keys(result.results).forEach((url: string) => {
result.results const permissionVerbResults = result.results[url];
)) { Object.keys(permissionVerbResults).forEach((permissionVerb: string) => {
for (const [permissionVerb, hasPermission] of Object.entries( const hasPermission = permissionVerbResults[permissionVerb];
permissionVerbResults
)) {
if (hasPermission) { if (hasPermission) {
can(permissionVerb, url); can(permissionVerb, url);
} else { } else {
cannot(permissionVerb, url); cannot(permissionVerb, url);
} }
} });
} });
ability.update(rules); ability.update(rules);
}; };
HttpService.makeCallToBackend({ HttpService.makeCallToBackend({

View File

@ -5,8 +5,10 @@ export const useUriListForPermissions = () => {
const targetUris = { const targetUris = {
processGroupListPath: `/v1.0/process-groups`, processGroupListPath: `/v1.0/process-groups`,
processGroupShowPath: `/v1.0/process-groups/${params.process_group_id}`, processGroupShowPath: `/v1.0/process-groups/${params.process_group_id}`,
processModelListPath: `/v1.0/process-models`, processModelCreatePath: `/v1.0/process-models/${params.process_group_id}`,
processModelShowPath: `/v1.0/process-models/${params.process_model_id}`, processModelShowPath: `/v1.0/process-models/${params.process_model_id}`,
processModelFileCreatePath: `/v1.0/process-models/${params.process_model_id}/files`,
processModelFileShowPath: `/v1.0/process-models/${params.process_model_id}/files/${params.file_name}`,
processInstanceListPath: `/v1.0/process-instances`, processInstanceListPath: `/v1.0/process-instances`,
processInstanceActionPath: `/v1.0/process-models/${params.process_model_id}/process-instances`, processInstanceActionPath: `/v1.0/process-models/${params.process_model_id}/process-instances`,
}; };

View File

@ -35,6 +35,8 @@ export default function ProcessGroupShow() {
const { targetUris } = useUriListForPermissions(); const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = { const permissionRequestData: PermissionsToCheck = {
[targetUris.processGroupListPath]: ['POST'], [targetUris.processGroupListPath]: ['POST'],
[targetUris.processGroupShowPath]: ['PUT'],
[targetUris.processModelCreatePath]: ['POST'],
}; };
const { ability } = usePermissionFetcher(permissionRequestData); const { ability } = usePermissionFetcher(permissionRequestData);
@ -159,23 +161,29 @@ export default function ProcessGroupShow() {
<Stack orientation="horizontal" gap={3}> <Stack orientation="horizontal" gap={3}>
<Can I="POST" a={targetUris.processGroupListPath} ability={ability}> <Can I="POST" a={targetUris.processGroupListPath} ability={ability}>
<Button <Button
kind="secondary"
href={`/admin/process-groups/new?parentGroupId=${processGroup.id}`} href={`/admin/process-groups/new?parentGroupId=${processGroup.id}`}
> >
Add a process group Add a process group
</Button> </Button>
</Can> </Can>
<Button <Can
href={`/admin/process-models/${modifiedProcessGroupId}/new`} I="POST"
a={targetUris.processModelCreatePath}
ability={ability}
> >
Add a process model <Button
</Button> href={`/admin/process-models/${modifiedProcessGroupId}/new`}
<Button >
href={`/admin/process-groups/${modifiedProcessGroupId}/edit`} Add a process model
variant="secondary" </Button>
> </Can>
Edit process group <Can I="PUT" a={targetUris.processGroupShowPath} ability={ability}>
</Button> <Button
href={`/admin/process-groups/${modifiedProcessGroupId}/edit`}
>
Edit process group
</Button>
</Can>
</Stack> </Stack>
<br /> <br />
<br /> <br />

View File

@ -106,9 +106,10 @@ export default function ProcessModelShow() {
const { targetUris } = useUriListForPermissions(); const { targetUris } = useUriListForPermissions();
const permissionRequestData: PermissionsToCheck = { const permissionRequestData: PermissionsToCheck = {
[targetUris.processModelShowPath]: ['GET', 'PUT'], [targetUris.processModelShowPath]: ['PUT'],
[targetUris.processInstanceListPath]: ['GET'], [targetUris.processInstanceListPath]: ['GET'],
[targetUris.processInstanceActionPath]: ['POST'], [targetUris.processInstanceActionPath]: ['POST'],
[targetUris.processModelFileCreatePath]: ['POST', 'GET', 'DELETE'],
}; };
const { ability } = usePermissionFetcher(permissionRequestData); const { ability } = usePermissionFetcher(permissionRequestData);
@ -263,50 +264,62 @@ export default function ProcessModelShow() {
) => { ) => {
const elements = []; const elements = [];
elements.push( elements.push(
<Button <Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
kind="ghost" <Button
renderIcon={Edit} kind="ghost"
iconDescription="Edit File" renderIcon={Edit}
hasIconOnly iconDescription="Edit File"
size="lg" hasIconOnly
data-qa={`edit-file-${processModelFile.name.replace('.', '-')}`} size="lg"
onClick={() => navigateToFileEdit(processModelFile)} data-qa={`edit-file-${processModelFile.name.replace('.', '-')}`}
/> onClick={() => navigateToFileEdit(processModelFile)}
/>
</Can>
); );
elements.push( elements.push(
<Button <Can I="GET" a={targetUris.processModelFileCreatePath} ability={ability}>
kind="ghost" <Button
renderIcon={Download} kind="ghost"
iconDescription="Download File" renderIcon={Download}
hasIconOnly iconDescription="Download File"
size="lg" hasIconOnly
onClick={() => downloadFile(processModelFile.name)} size="lg"
/> onClick={() => downloadFile(processModelFile.name)}
/>
</Can>
); );
elements.push( elements.push(
<ButtonWithConfirmation <Can
kind="ghost" I="DELETE"
renderIcon={TrashCan} a={targetUris.processModelFileCreatePath}
iconDescription="Delete File" ability={ability}
hasIconOnly >
description={`Delete file: ${processModelFile.name}`} <ButtonWithConfirmation
onConfirmation={() => { kind="ghost"
onDeleteFile(processModelFile.name); renderIcon={TrashCan}
}} iconDescription="Delete File"
confirmButtonLabel="Delete" hasIconOnly
/> description={`Delete file: ${processModelFile.name}`}
onConfirmation={() => {
onDeleteFile(processModelFile.name);
}}
confirmButtonLabel="Delete"
/>
</Can>
); );
if (processModelFile.name.match(/\.bpmn$/) && !isPrimaryBpmnFile) { if (processModelFile.name.match(/\.bpmn$/) && !isPrimaryBpmnFile) {
elements.push( elements.push(
<Button <Can I="PUT" a={targetUris.processModelShowPath} ability={ability}>
kind="ghost" <Button
renderIcon={Favorite} kind="ghost"
iconDescription="Set As Primary File" renderIcon={Favorite}
hasIconOnly iconDescription="Set As Primary File"
size="lg" hasIconOnly
onClick={() => onSetPrimaryFile(processModelFile.name)} size="lg"
/> onClick={() => onSetPrimaryFile(processModelFile.name)}
/>
</Can>
); );
} }
return elements; return elements;
@ -337,7 +350,11 @@ export default function ProcessModelShow() {
let fileLink = null; let fileLink = null;
const fileUrl = profileModelFileEditUrl(processModelFile); const fileUrl = profileModelFileEditUrl(processModelFile);
if (fileUrl) { if (fileUrl) {
fileLink = <Link to={fileUrl}>{processModelFile.name}</Link>; if (ability.can('GET', targetUris.processModelFileCreatePath)) {
fileLink = <Link to={fileUrl}>{processModelFile.name}</Link>;
} else {
fileLink = <span>{processModelFile.name}</span>;
}
} }
constructedTag = ( constructedTag = (
<TableRow key={processModelFile.name}> <TableRow key={processModelFile.name}>
@ -442,47 +459,53 @@ export default function ProcessModelShow() {
</Stack> </Stack>
} }
> >
<ButtonSet> <Can
<Button I="POST"
renderIcon={Upload} a={targetUris.processModelFileCreatePath}
data-qa="upload-file-button" ability={ability}
onClick={() => setShowFileUploadModal(true)} >
size="sm" <ButtonSet>
kind="" <Button
className="button-white-background" renderIcon={Upload}
> data-qa="upload-file-button"
Upload File onClick={() => setShowFileUploadModal(true)}
</Button> size="sm"
<Button kind=""
renderIcon={Add} className="button-white-background"
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=bpmn`} >
size="sm" Upload File
> </Button>
New BPMN File <Button
</Button> renderIcon={Add}
<Button href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=bpmn`}
renderIcon={Add} size="sm"
href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=dmn`} >
size="sm" New BPMN File
> </Button>
New DMN File <Button
</Button> renderIcon={Add}
<Button href={`/admin/process-models/${modifiedProcessModelId}/files?file_type=dmn`}
renderIcon={Add} size="sm"
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=json`} >
size="sm" New DMN File
> </Button>
New JSON File <Button
</Button> renderIcon={Add}
<Button href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=json`}
renderIcon={Add} size="sm"
href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=md`} >
size="sm" New JSON File
> </Button>
New Markdown File <Button
</Button> renderIcon={Add}
</ButtonSet> href={`/admin/process-models/${modifiedProcessModelId}/form?file_ext=md`}
<br /> size="sm"
>
New Markdown File
</Button>
</ButtonSet>
<br />
</Can>
{processModelFileList()} {processModelFileList()}
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>