From 4758634c99407015b96785f32b48d300d2ce77ba Mon Sep 17 00:00:00 2001 From: jbirddog <100367399+jbirddog@users.noreply.github.com> Date: Wed, 24 Jan 2024 10:15:08 -0500 Subject: [PATCH] Show Data Store tiles on the Process Group page (#917) Co-authored-by: Kevin Burnett <18027+burnettk@users.noreply.github.com> --- .../src/spiffworkflow_backend/api.yml | 72 +++++++++++- .../spiffworkflow_backend/data_stores/crud.py | 10 +- .../spiffworkflow_backend/data_stores/json.py | 28 +++-- .../spiffworkflow_backend/data_stores/kkv.py | 12 +- .../data_stores/typeahead.py | 12 +- .../routes/data_store_controller.py | 61 +++++++++- .../integration/test_data_stores.py | 6 +- .../src/components/DataStoreForm.tsx | 48 ++++---- .../src/components/DataStoreListTable.tsx | 2 +- .../src/components/DataStoreListTiles.tsx | 110 ++++++++++++++++++ spiffworkflow-frontend/src/index.css | 26 +++++ .../src/routes/DataStoreEdit.tsx | 58 +++++++++ .../src/routes/DataStoreRoutes.tsx | 2 + .../src/routes/ProcessGroupShow.tsx | 12 ++ 14 files changed, 398 insertions(+), 61 deletions(-) create mode 100644 spiffworkflow-frontend/src/components/DataStoreListTiles.tsx create mode 100644 spiffworkflow-frontend/src/routes/DataStoreEdit.tsx diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index aa6a1dac..07d9747f 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -305,7 +305,7 @@ paths: - name: per_page in: query required: false - description: The number of groups to show per page. Defaults to page 10. + description: The number of groups to show per page. Defaults to 10. schema: type: integer get: @@ -449,7 +449,7 @@ paths: - name: per_page in: query required: false - description: The number of models to show per page. Defaults to page 10. + description: The number of models to show per page. Defaults to 10. schema: type: integer get: @@ -2439,7 +2439,7 @@ paths: - name: per_page in: query required: false - description: The number of models to show per page. Defaults to page 10. + description: The number of models to show per page. Defaults to 10. schema: type: integer get: @@ -2498,7 +2498,7 @@ paths: - name: per_page in: query required: false - description: The number of items to show per page. Defaults to page 10. + description: The number of items to show per page. Defaults to 10. schema: type: integer - name: events @@ -2621,7 +2621,7 @@ paths: - name: per_page in: query required: false - description: The number of items to show per page. Defaults to page 10. + description: The number of items to show per page. Defaults to 10. schema: type: integer post: @@ -2759,6 +2759,25 @@ paths: # - application/json /data-stores: + parameters: + - name: process_group_identifier + in: query + required: false + description: Optional parameter to filter by a single group + schema: + type: string + - name: page + in: query + required: false + description: The page number to return. Defaults to page 1. + schema: + type: integer + - name: per_page + in: query + required: false + description: The number of groups to show per page. Defaults to 10. + schema: + type: integer get: operationId: spiffworkflow_backend.routes.data_store_controller.data_store_list summary: Return a list of the data store objects. @@ -2780,6 +2799,19 @@ paths: responses: "200": description: The newly created data store instance + put: + operationId: spiffworkflow_backend.routes.data_store_controller.data_store_update + summary: Update a data store instance. + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/DataStore" + tags: + - Data Stores + responses: + "200": + description: The updated data store instance /data-stores/types: get: operationId: spiffworkflow_backend.routes.data_store_controller.data_store_types @@ -2789,7 +2821,7 @@ paths: responses: "200": description: The list of currently defined data store types - /data-stores/{data_store_type}/{name}: + /data-stores/{data_store_type}/{name}/items: parameters: - name: data_store_type in: path @@ -2823,6 +2855,34 @@ paths: responses: "200": description: A list of the data stored in the requested data store. + /data-stores/{data_store_type}/{identifier}: + parameters: + - name: data_store_type + in: path + required: true + description: The type of datastore, such as "typeahead" + schema: + type: string + - name: identifier + in: path + required: true + description: The identifier of the datastore, such as "cities" + schema: + type: string + - name: process_group_identifier + in: query + required: true + description: The process group + schema: + type: string + get: + operationId: spiffworkflow_backend.routes.data_store_controller.data_store_show + summary: Returns a description of the data store. + tags: + - Data Stores + responses: + "200": + description: The requested data store. components: securitySchemes: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/data_stores/crud.py b/spiffworkflow-backend/src/spiffworkflow_backend/data_stores/crud.py index c014545b..9721ce47 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/data_stores/crud.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/data_stores/crud.py @@ -3,15 +3,19 @@ from typing import Any class DataStoreCRUD: @staticmethod - def create_instance(name: str, identifier: str, location: str, schema: dict[str, Any], description: str | None) -> None: + def create_instance(identifier: str, location: str) -> Any: raise Exception("must implement") @staticmethod - def existing_data_stores() -> list[dict[str, Any]]: + def existing_instance(identifier: str, location: str) -> Any: raise Exception("must implement") @staticmethod - def query_data_store(name: str) -> Any: + def existing_data_stores(process_group_identifier: str | None = None) -> list[dict[str, Any]]: + raise Exception("must implement") + + @staticmethod + def get_data_store_query(name: str, process_group_identifier: str | None) -> Any: raise Exception("must implement") @staticmethod diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/data_stores/json.py b/spiffworkflow-backend/src/spiffworkflow_backend/data_stores/json.py index 284cd148..46f56ed9 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/data_stores/json.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/data_stores/json.py @@ -33,32 +33,38 @@ class JSONDataStore(BpmnDataStoreSpecification, DataStoreCRUD): # type: ignore """JSONDataStore.""" @staticmethod - def create_instance(name: str, identifier: str, location: str, schema: dict[str, Any], description: str | None) -> None: - model = JSONDataStoreModel( - name=name, + def create_instance(identifier: str, location: str) -> Any: + return JSONDataStoreModel( identifier=identifier, location=location, - schema=schema, - description=description or "", data={}, ) - db.session.add(model) - db.session.commit() @staticmethod - def existing_data_stores() -> list[dict[str, Any]]: + def existing_instance(identifier: str, location: str) -> Any: + return db.session.query(JSONDataStoreModel).filter_by(identifier=identifier, location=location).first() + + @staticmethod + def existing_data_stores(process_group_identifier: str | None = None) -> list[dict[str, Any]]: data_stores = [] query = db.session.query(JSONDataStoreModel.name, JSONDataStoreModel.identifier) + if process_group_identifier is not None: + query = query.filter_by(location=process_group_identifier) keys = query.distinct().order_by(JSONDataStoreModel.name).all() # type: ignore for key in keys: - data_stores.append({"name": key[0], "type": "json", "identifier": key[1], "clz": "JSONDataStore"}) + data_stores.append({"name": key[0], "type": "json", "id": key[1], "clz": "JSONDataStore"}) return data_stores @staticmethod - def query_data_store(name: str) -> Any: - return JSONDataStoreModel.query.filter_by(name=name).order_by(JSONDataStoreModel.name) + def get_data_store_query(identifier: str, process_group_identifier: str | None) -> Any: + query = JSONDataStoreModel.query + if process_group_identifier is not None: + query = query.filter_by(identifier=identifier, location=process_group_identifier) + else: + query = query.filter_by(name=identifier) + return query.order_by(JSONDataStoreModel.name) @staticmethod def build_response_item(model: Any) -> dict[str, Any]: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/data_stores/kkv.py b/spiffworkflow-backend/src/spiffworkflow_backend/data_stores/kkv.py index e690cec8..e2d220e3 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/data_stores/kkv.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/data_stores/kkv.py @@ -13,8 +13,12 @@ class KKVDataStore(BpmnDataStoreSpecification, DataStoreCRUD): # type: ignore """KKVDataStore.""" @staticmethod - def existing_data_stores() -> list[dict[str, Any]]: - data_stores = [] + def existing_data_stores(process_group_identifier: str | None = None) -> list[dict[str, Any]]: + data_stores: list[dict[str, Any]] = [] + + if process_group_identifier is not None: + # temporary until this data store gets location support + return data_stores keys = ( db.session.query(KKVDataStoreModel.top_level_key) @@ -23,12 +27,12 @@ class KKVDataStore(BpmnDataStoreSpecification, DataStoreCRUD): # type: ignore .all() ) for key in keys: - data_stores.append({"name": key[0], "type": "kkv", "identifier": "", "clz": "KKVDataStore"}) + data_stores.append({"name": key[0], "type": "kkv", "id": "", "clz": "KKVDataStore"}) return data_stores @staticmethod - def query_data_store(name: str) -> Any: + def get_data_store_query(name: str, process_group_identifier: str | None) -> Any: return KKVDataStoreModel.query.filter_by(top_level_key=name).order_by( KKVDataStoreModel.top_level_key, KKVDataStoreModel.secondary_key ) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/data_stores/typeahead.py b/spiffworkflow-backend/src/spiffworkflow_backend/data_stores/typeahead.py index 3bc579c4..5f8b26c7 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/data_stores/typeahead.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/data_stores/typeahead.py @@ -14,17 +14,21 @@ class TypeaheadDataStore(BpmnDataStoreSpecification, DataStoreCRUD): # type: ig """TypeaheadDataStore.""" @staticmethod - def existing_data_stores() -> list[dict[str, Any]]: - data_stores = [] + def existing_data_stores(process_group_identifier: str | None = None) -> list[dict[str, Any]]: + data_stores: list[dict[str, Any]] = [] + + if process_group_identifier is not None: + # temporary until this data store gets location support + return data_stores keys = db.session.query(TypeaheadModel.category).distinct().order_by(TypeaheadModel.category).all() # type: ignore for key in keys: - data_stores.append({"name": key[0], "type": "typeahead", "identifier": key[0], "clz": "TypeaheadDataStore"}) + data_stores.append({"name": key[0], "type": "typeahead", "id": key[0], "clz": "TypeaheadDataStore"}) return data_stores @staticmethod - def query_data_store(name: str) -> Any: + def get_data_store_query(name: str, process_group_identifier: str | None) -> Any: return TypeaheadModel.query.filter_by(category=name).order_by(TypeaheadModel.category, TypeaheadModel.search_term) @staticmethod diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/data_store_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/data_store_controller.py index d38ddbfa..85e4da92 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/data_store_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/data_store_controller.py @@ -10,6 +10,7 @@ from spiffworkflow_backend.data_stores.json import JSONDataStore from spiffworkflow_backend.data_stores.kkv import KKVDataStore from spiffworkflow_backend.data_stores.typeahead import TypeaheadDataStore from spiffworkflow_backend.exceptions.api_error import ApiError +from spiffworkflow_backend.models.db import db DATA_STORES = { "json": (JSONDataStore, "JSON Data Store"), @@ -18,15 +19,15 @@ DATA_STORES = { } -def data_store_list() -> flask.wrappers.Response: +def data_store_list(process_group_identifier: str | None = None, page: int = 1, per_page: int = 100) -> flask.wrappers.Response: """Returns a list of the names of all the data stores.""" data_stores = [] # Right now the only data stores we support are type ahead, kkv, json - data_stores.extend(JSONDataStore.existing_data_stores()) - data_stores.extend(TypeaheadDataStore.existing_data_stores()) - data_stores.extend(KKVDataStore.existing_data_stores()) + data_stores.extend(JSONDataStore.existing_data_stores(process_group_identifier)) + data_stores.extend(TypeaheadDataStore.existing_data_stores(process_group_identifier)) + data_stores.extend(KKVDataStore.existing_data_stores(process_group_identifier)) return make_response(jsonify(data_stores), 200) @@ -42,7 +43,7 @@ def data_store_types() -> flask.wrappers.Response: def _build_response(data_store_class: Any, name: str, page: int, per_page: int) -> flask.wrappers.Response: - data_store_query = data_store_class.query_data_store(name) + data_store_query = data_store_class.get_data_store_query(name, None) data = data_store_query.paginate(page=page, per_page=per_page, error_out=False) results = [] for item in data.items: @@ -70,6 +71,14 @@ def data_store_item_list(data_store_type: str, name: str, page: int = 1, per_pag def data_store_create(body: dict) -> flask.wrappers.Response: + return _data_store_upsert(body, True) + + +def data_store_update(body: dict) -> flask.wrappers.Response: + return _data_store_upsert(body, False) + + +def _data_store_upsert(body: dict, insert: bool) -> flask.wrappers.Response: try: data_store_type = body["type"] name = body["name"] @@ -97,6 +106,46 @@ def data_store_create(body: dict) -> flask.wrappers.Response: raise ApiError("unknown_data_store", f"Unknown data store type: {data_store_type}", status_code=400) data_store_class, _ = DATA_STORES[data_store_type] - data_store_class.create_instance(name, identifier, location, schema, description) + + if insert: + model = data_store_class.create_instance(identifier, location) + else: + model = data_store_class.existing_instance(identifier, location) + + model.name = name + model.schema = schema + model.description = description or "" + + db.session.add(model) + db.session.commit() return make_response(jsonify({"ok": True}), 200) + + +def data_store_show(data_store_type: str, identifier: str, process_group_identifier: str) -> flask.wrappers.Response: + """Returns a description of a data store.""" + + if data_store_type not in DATA_STORES: + raise ApiError("unknown_data_store", f"Unknown data store type: {data_store_type}", status_code=400) + + data_store_class, _ = DATA_STORES[data_store_type] + data_store_query = data_store_class.get_data_store_query(identifier, process_group_identifier) + result = data_store_query.first() + + if result is None: + raise ApiError( + "could_not_locate_data_store", + f"Could not locate data store type: {data_store_type} for process group: {process_group_identifier}", + status_code=400, + ) + + response = { + "name": result.name, + "location": result.location, + "type": data_store_type, + "id": result.identifier, + "schema": result.schema, + "description": result.description, + } + + return make_response(jsonify(response), 200) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_data_stores.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_data_stores.py index 3b313072..a546dfa2 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_data_stores.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_data_stores.py @@ -67,8 +67,8 @@ class TestDataStores(BaseTest): self.load_data_store(app, client, with_db_and_bpmn_file_cleanup, with_super_admin_user) results = client.get("/v1.0/data-stores", headers=self.logged_in_headers(with_super_admin_user)) assert results.json == [ - {"name": "albums", "type": "typeahead", "identifier": "albums", "clz": "TypeaheadDataStore"}, - {"name": "cereals", "type": "typeahead", "identifier": "cereals", "clz": "TypeaheadDataStore"}, + {"name": "albums", "type": "typeahead", "id": "albums", "clz": "TypeaheadDataStore"}, + {"name": "cereals", "type": "typeahead", "id": "cereals", "clz": "TypeaheadDataStore"}, ] def test_get_data_store_returns_paginated_results( @@ -80,7 +80,7 @@ class TestDataStores(BaseTest): ) -> None: self.load_data_store(app, client, with_db_and_bpmn_file_cleanup, with_super_admin_user) response = client.get( - "/v1.0/data-stores/typeahead/albums?per_page=10", headers=self.logged_in_headers(with_super_admin_user) + "/v1.0/data-stores/typeahead/albums/items?per_page=10", headers=self.logged_in_headers(with_super_admin_user) ) expected_item_in_response = { diff --git a/spiffworkflow-frontend/src/components/DataStoreForm.tsx b/spiffworkflow-frontend/src/components/DataStoreForm.tsx index a2e17cad..fde106f5 100644 --- a/spiffworkflow-frontend/src/components/DataStoreForm.tsx +++ b/spiffworkflow-frontend/src/components/DataStoreForm.tsx @@ -49,7 +49,11 @@ export default function DataStoreForm({ useEffect(() => { const handleSetDataStoreTypesCallback = (result: any) => { + const dataStoreType = result.find((item: any) => { + return item.type === dataStore.type; + }); setDataStoreTypes(result); + setSelectedDataStoreType(dataStoreType ?? null); }; HttpService.makeCallToBackend({ @@ -57,7 +61,7 @@ export default function DataStoreForm({ successCallback: handleSetDataStoreTypesCallback, httpMethod: 'GET', }); - }, [setDataStoreTypes]); + }, [dataStore, setDataStoreTypes]); const navigateToDataStores = (_result: any) => { const location = dataStoreLocation(); @@ -99,10 +103,9 @@ export default function DataStoreForm({ if (hasErrors) { return; } - let path = '/data-stores'; + const path = '/data-stores'; let httpMethod = 'POST'; if (mode === 'edit') { - path = `/data-stores/${dataStore.id}`; httpMethod = 'PUT'; } const postBody = { @@ -177,26 +180,25 @@ export default function DataStoreForm({ />, ]; - if (mode === 'new') { - textInputs.push( - { - updateDataStore({ id: event.target.value }); - // was invalid, and now valid - if (identifierInvalid && hasValidIdentifier(event.target.value)) { - setIdentifierInvalid(false); - } - setIdHasBeenUpdatedByUser(true); - }} - /> - ); - } + textInputs.push( + { + updateDataStore({ id: event.target.value }); + // was invalid, and now valid + if (identifierInvalid && hasValidIdentifier(event.target.value)) { + setIdentifierInvalid(false); + } + setIdHasBeenUpdatedByUser(true); + }} + /> + ); textInputs.push( { setResults(response.results); setPagination(response.pagination); diff --git a/spiffworkflow-frontend/src/components/DataStoreListTiles.tsx b/spiffworkflow-frontend/src/components/DataStoreListTiles.tsx new file mode 100644 index 00000000..99d059d9 --- /dev/null +++ b/spiffworkflow-frontend/src/components/DataStoreListTiles.tsx @@ -0,0 +1,110 @@ +import { ReactElement, useEffect, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { ArrowRight } from '@carbon/icons-react'; +import { ClickableTile } from '@carbon/react'; +import HttpService from '../services/HttpService'; +import { DataStore, ProcessGroup } from '../interfaces'; +import { truncateString } from '../helpers'; + +type OwnProps = { + defaultDataStores?: DataStore[]; + dataStore?: DataStore; + processGroup?: ProcessGroup; + headerElement?: ReactElement; + showNoItemsDisplayText?: boolean; + userCanCreateDataStores?: boolean; +}; + +export default function DataStoreListTiles({ + defaultDataStores, + dataStore, + processGroup, + headerElement, + showNoItemsDisplayText, + userCanCreateDataStores, +}: OwnProps) { + const [searchParams] = useSearchParams(); + + const [dataStores, setDataStores] = useState(null); + + useEffect(() => { + const setDataStoresFromResult = (result: any) => { + setDataStores(result); + }; + + if (defaultDataStores) { + setDataStores(defaultDataStores); + } else { + let queryParams = '?per_page=1000'; + if (processGroup) { + queryParams = `${queryParams}&process_group_identifier=${processGroup.id}`; + } + HttpService.makeCallToBackend({ + path: `/data-stores${queryParams}`, + successCallback: setDataStoresFromResult, + }); + } + }, [searchParams, dataStore, defaultDataStores, processGroup]); + + const dataStoresDisplayArea = () => { + let displayText = null; + if (dataStores && dataStores.length > 0) { + displayText = dataStores.map((row: DataStore) => { + return ( + +
+ +
{row.name}
+

+ {truncateString(row.description || '', 100)} +

+
+
+ ); + }); + } else if (userCanCreateDataStores) { + displayText = ( +

+ There are no data stores to display. You can add one by clicking the + "Add a data store" button. +

+ ); + } else { + displayText = ( +

+ There are no data stores to display. +

+ ); + } + return displayText; + }; + + const dataStoreArea = () => { + if (dataStores && (showNoItemsDisplayText || dataStores.length > 0)) { + return ( + <> + {headerElement} + {dataStoresDisplayArea()} + + ); + } + return null; + }; + + if (dataStores) { + return ( + <> + {/* so we can check if the data stores have loaded in cypress tests */} +
+ {dataStoreArea()} + + ); + } + return null; +} diff --git a/spiffworkflow-frontend/src/index.css b/spiffworkflow-frontend/src/index.css index 0cc41756..098e05f0 100644 --- a/spiffworkflow-frontend/src/index.css +++ b/spiffworkflow-frontend/src/index.css @@ -400,6 +400,32 @@ in on this with the react-jsonschema-form repo. This is just a patch fix to allo padding-left: 5px; } +.cds--tile.tile-data-store { + padding: 0px; + margin: 12px 24px 12px 0px; + width: 320px; + height: 264px; + background: #f4f4f4; + order: 1; + float: left; +} + +.tile-data-store-content-container { + width: 320px; + height: 264px; + padding: 1rem; + position: relative; +} + +.tile-data-store-display-name { + margin-top: 2rem; + margin-bottom: 1rem; + font-size: 20px; + line-height: 28px; + color: #161616; + order: 0; +} + .cds--tile.tile-process-group { padding: 0px; margin: 12px 24px 12px 0px; diff --git a/spiffworkflow-frontend/src/routes/DataStoreEdit.tsx b/spiffworkflow-frontend/src/routes/DataStoreEdit.tsx new file mode 100644 index 00000000..8489dcf1 --- /dev/null +++ b/spiffworkflow-frontend/src/routes/DataStoreEdit.tsx @@ -0,0 +1,58 @@ +import { useEffect, useState } from 'react'; +import { useParams, useSearchParams } from 'react-router-dom'; +import ProcessBreadcrumb from '../components/ProcessBreadcrumb'; +import DataStoreForm from '../components/DataStoreForm'; +import { DataStore, HotCrumbItem } from '../interfaces'; +import { setPageTitle } from '../helpers'; +import HttpService from '../services/HttpService'; + +export default function DataStoreEdit() { + const params = useParams(); + const [searchParams] = useSearchParams(); + const parentGroupId = searchParams.get('parentGroupId'); + const dataStoreIdentifier = params.data_store_identifier; + const [dataStore, setDataStore] = useState({ + id: '', + name: '', + type: '', + schema: '', + description: '', + }); + useEffect(() => { + setPageTitle(['Edit Data Store']); + }, []); + + useEffect(() => { + const setDataStoreFromResult = (result: any) => { + const schema = JSON.stringify(result.schema); + setDataStore({ ...result, schema }); + }; + + const queryParams = `?process_group_identifier=${parentGroupId}`; + HttpService.makeCallToBackend({ + path: `/data-stores/json/${dataStoreIdentifier}${queryParams}`, + successCallback: setDataStoreFromResult, + }); + }, [dataStoreIdentifier, parentGroupId]); + + const hotCrumbs: HotCrumbItem[] = [['Process Groups', '/process-groups']]; + if (parentGroupId) { + hotCrumbs.push({ + entityToExplode: parentGroupId, + entityType: 'process-group-id', + linkLastItem: true, + }); + } + + return ( + <> + +

Edit Data Store

+ + + ); +} diff --git a/spiffworkflow-frontend/src/routes/DataStoreRoutes.tsx b/spiffworkflow-frontend/src/routes/DataStoreRoutes.tsx index 0cb12389..6a58c6a9 100644 --- a/spiffworkflow-frontend/src/routes/DataStoreRoutes.tsx +++ b/spiffworkflow-frontend/src/routes/DataStoreRoutes.tsx @@ -1,11 +1,13 @@ import { Route, Routes } from 'react-router-dom'; import DataStoreList from './DataStoreList'; import DataStoreNew from './DataStoreNew'; +import DataStoreEdit from './DataStoreEdit'; export default function DataStoreRoutes() { return ( } /> + } /> } /> ); diff --git a/spiffworkflow-frontend/src/routes/ProcessGroupShow.tsx b/spiffworkflow-frontend/src/routes/ProcessGroupShow.tsx index 620d10dc..83b2584b 100644 --- a/spiffworkflow-frontend/src/routes/ProcessGroupShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessGroupShow.tsx @@ -14,6 +14,7 @@ import { PermissionsToCheck, ProcessGroup } from '../interfaces'; import { useUriListForPermissions } from '../hooks/UriListForPermissions'; import { usePermissionFetcher } from '../hooks/PermissionService'; import ProcessGroupListTiles from '../components/ProcessGroupListTiles'; +import DataStoreListTiles from '../components/DataStoreListTiles'; import ButtonWithConfirmation from '../components/ButtonWithConfirmation'; import ProcessModelListTiles from '../components/ProcessModelListTiles'; import useProcessGroupFetcher from '../hooks/useProcessGroupFetcher'; @@ -162,6 +163,17 @@ export default function ProcessGroupShow() { targetUris.processGroupListPath )} /> +
+
+ Data Stores} + showNoItemsDisplayText={showNoItemsDisplayText} + userCanCreateDataStores={ability.can( + 'POST', + targetUris.dataStoreListPath + )} + /> );