Add data store at the process group level (#859)
This commit is contained in:
parent
ab39569cac
commit
a8a32b60fa
|
@ -0,0 +1,45 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 12a8864399d4
|
||||||
|
Revises: bc2b84d013e0
|
||||||
|
Create Date: 2023-12-19 08:07:12.265442
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '12a8864399d4'
|
||||||
|
down_revision = 'bc2b84d013e0'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('json_data_store', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index('ix_json_data_store_name')
|
||||||
|
|
||||||
|
op.drop_table('json_data_store')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('json_data_store',
|
||||||
|
sa.Column('id', mysql.INTEGER(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('name', mysql.VARCHAR(length=255), nullable=True),
|
||||||
|
sa.Column('location', mysql.VARCHAR(length=255), nullable=True),
|
||||||
|
sa.Column('data', mysql.JSON(), nullable=True),
|
||||||
|
sa.Column('updated_at_in_seconds', mysql.INTEGER(), autoincrement=False, nullable=True),
|
||||||
|
sa.Column('created_at_in_seconds', mysql.INTEGER(), autoincrement=False, nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
mysql_collate='utf8mb4_0900_ai_ci',
|
||||||
|
mysql_default_charset='utf8mb4',
|
||||||
|
mysql_engine='InnoDB'
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('json_data_store', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('ix_json_data_store_name', ['name'], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
|
@ -0,0 +1,48 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: a872f8f2e909
|
||||||
|
Revises: 12a8864399d4
|
||||||
|
Create Date: 2023-12-19 08:40:26.572613
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'a872f8f2e909'
|
||||||
|
down_revision = '12a8864399d4'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('json_data_store',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('identifier', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('location', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('schema', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('data', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('description', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('updated_at_in_seconds', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at_in_seconds', sa.Integer(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('identifier', 'location', name='_identifier_location_unique')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('json_data_store', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_json_data_store_identifier'), ['identifier'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_json_data_store_name'), ['name'], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('json_data_store', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_json_data_store_name'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_json_data_store_identifier'))
|
||||||
|
|
||||||
|
op.drop_table('json_data_store')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -2770,6 +2770,28 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: The list of currently defined data store objects
|
description: The list of currently defined data store objects
|
||||||
|
post:
|
||||||
|
operationId: spiffworkflow_backend.routes.data_store_controller.data_store_create
|
||||||
|
summary: Create a new data store instance.
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/DataStore"
|
||||||
|
tags:
|
||||||
|
- Data Stores
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The newly created data store instance
|
||||||
|
/data-stores/types:
|
||||||
|
get:
|
||||||
|
operationId: spiffworkflow_backend.routes.data_store_controller.data_store_types
|
||||||
|
summary: Return a list of the data store types.
|
||||||
|
tags:
|
||||||
|
- Data Stores
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The list of currently defined data store types
|
||||||
/data-stores/{data_store_type}/{name}:
|
/data-stores/{data_store_type}/{name}:
|
||||||
parameters:
|
parameters:
|
||||||
- name: data_store_type
|
- name: data_store_type
|
||||||
|
@ -2998,31 +3020,22 @@ components:
|
||||||
DataStore:
|
DataStore:
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: integer
|
|
||||||
example: 1234
|
|
||||||
key:
|
|
||||||
type: string
|
type: string
|
||||||
example: MyKey
|
example: employees
|
||||||
workflow_id:
|
name:
|
||||||
type: integer
|
type: string
|
||||||
x-nullable: true
|
example: Emplyoees DataStore
|
||||||
example: 12
|
type:
|
||||||
user_id:
|
type: string
|
||||||
|
example: TypeaheadDataStore
|
||||||
|
description:
|
||||||
type: string
|
type: string
|
||||||
x-nullable: true
|
x-nullable: true
|
||||||
example: dhf8r
|
example: This data store contains all the employees
|
||||||
task_id:
|
parent_group_id:
|
||||||
type: string
|
type: string
|
||||||
x-nullable: true
|
x-nullable: true
|
||||||
example: MyTask
|
example: Optional parent group id to specify the location of this data store
|
||||||
process_model_id:
|
|
||||||
type: string
|
|
||||||
x-nullable: true
|
|
||||||
example: My Spec Name
|
|
||||||
value:
|
|
||||||
type: string
|
|
||||||
x-nullable: true
|
|
||||||
example: Some Value
|
|
||||||
Process:
|
Process:
|
||||||
properties:
|
properties:
|
||||||
identifier:
|
identifier:
|
||||||
|
|
|
@ -2,6 +2,10 @@ from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class DataStoreCRUD:
|
class DataStoreCRUD:
|
||||||
|
@staticmethod
|
||||||
|
def create_instance(name: str, identifier: str, location: str, schema: dict[str, Any], description: str | None) -> None:
|
||||||
|
raise Exception("must implement")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def existing_data_stores() -> list[dict[str, Any]]:
|
def existing_data_stores() -> list[dict[str, Any]]:
|
||||||
raise Exception("must implement")
|
raise Exception("must implement")
|
||||||
|
|
|
@ -10,6 +10,15 @@ from spiffworkflow_backend.models.db import db
|
||||||
from spiffworkflow_backend.models.json_data_store import JSONDataStoreModel
|
from spiffworkflow_backend.models.json_data_store import JSONDataStoreModel
|
||||||
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
||||||
from spiffworkflow_backend.services.reference_cache_service import ReferenceCacheService
|
from spiffworkflow_backend.services.reference_cache_service import ReferenceCacheService
|
||||||
|
from spiffworkflow_backend.services.upsearch_service import UpsearchService
|
||||||
|
|
||||||
|
|
||||||
|
class DataStoreReadError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DataStoreWriteError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _process_model_location_for_task(spiff_task: SpiffTask) -> str | None:
|
def _process_model_location_for_task(spiff_task: SpiffTask) -> str | None:
|
||||||
|
@ -19,29 +28,22 @@ def _process_model_location_for_task(spiff_task: SpiffTask) -> str | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _data_store_filename(name: str) -> str:
|
|
||||||
return f"{name}.json"
|
|
||||||
|
|
||||||
|
|
||||||
def _data_store_exists_at_location(location: str, name: str) -> bool:
|
|
||||||
return FileSystemService.file_exists_at_relative_path(location, _data_store_filename(name))
|
|
||||||
|
|
||||||
|
|
||||||
def _data_store_location_for_task(spiff_task: SpiffTask, name: str) -> str | None:
|
|
||||||
location = _process_model_location_for_task(spiff_task)
|
|
||||||
if location is None:
|
|
||||||
return None
|
|
||||||
if _data_store_exists_at_location(location, name):
|
|
||||||
return location
|
|
||||||
location = ReferenceCacheService.upsearch(location, name, "data_store")
|
|
||||||
if location is None or not _data_store_exists_at_location(location, name):
|
|
||||||
return None
|
|
||||||
return location
|
|
||||||
|
|
||||||
|
|
||||||
class JSONDataStore(BpmnDataStoreSpecification, DataStoreCRUD): # type: ignore
|
class JSONDataStore(BpmnDataStoreSpecification, DataStoreCRUD): # type: ignore
|
||||||
"""JSONDataStore."""
|
"""JSONDataStore."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_instance(name: str, identifier: str, location: str, schema: dict[str, Any], description: str | None) -> None:
|
||||||
|
model = JSONDataStoreModel(
|
||||||
|
name=name,
|
||||||
|
identifier=identifier,
|
||||||
|
location=location,
|
||||||
|
schema=schema,
|
||||||
|
description=description or "",
|
||||||
|
data={},
|
||||||
|
)
|
||||||
|
db.session.add(model)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def existing_data_stores() -> list[dict[str, Any]]:
|
def existing_data_stores() -> list[dict[str, Any]]:
|
||||||
data_stores = []
|
data_stores = []
|
||||||
|
@ -58,35 +60,55 @@ class JSONDataStore(BpmnDataStoreSpecification, DataStoreCRUD): # type: ignore
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def build_response_item(model: Any) -> dict[str, Any]:
|
def build_response_item(model: Any) -> dict[str, Any]:
|
||||||
return {"location": model.location, "data": model.data}
|
return {"location": model.location, "identifier": model.identifier, "data": model.data}
|
||||||
|
|
||||||
def get(self, my_task: SpiffTask) -> None:
|
def get(self, my_task: SpiffTask) -> None:
|
||||||
"""get."""
|
"""get."""
|
||||||
model: JSONDataStoreModel | None = None
|
model: JSONDataStoreModel | None = None
|
||||||
location = _data_store_location_for_task(my_task, self.bpmn_id)
|
location = self._data_store_location_for_task(my_task, self.bpmn_id)
|
||||||
if location is not None:
|
if location is not None:
|
||||||
model = db.session.query(JSONDataStoreModel).filter_by(name=self.bpmn_id, location=location).first()
|
model = db.session.query(JSONDataStoreModel).filter_by(identifier=self.bpmn_id, location=location).first()
|
||||||
if model is None:
|
if model is None:
|
||||||
raise Exception(f"Unable to read from data store '{self.bpmn_id}' using location '{location}'.")
|
raise DataStoreReadError(f"Unable to read from data store '{self.bpmn_id}' using location '{location}'.")
|
||||||
my_task.data[self.bpmn_id] = model.data
|
my_task.data[self.bpmn_id] = model.data
|
||||||
|
|
||||||
def set(self, my_task: SpiffTask) -> None:
|
def set(self, my_task: SpiffTask) -> None:
|
||||||
"""set."""
|
"""set."""
|
||||||
location = _data_store_location_for_task(my_task, self.bpmn_id)
|
model: JSONDataStoreModel | None = None
|
||||||
if location is None:
|
location = self._data_store_location_for_task(my_task, self.bpmn_id)
|
||||||
raise Exception(f"Unable to write to data store '{self.bpmn_id}' using location '{location}'.")
|
|
||||||
data = my_task.data[self.bpmn_id]
|
if location is not None:
|
||||||
model = JSONDataStoreModel(
|
model = JSONDataStoreModel.query.filter_by(identifier=self.bpmn_id, location=location).first()
|
||||||
name=self.bpmn_id,
|
if location is None or model is None:
|
||||||
location=location,
|
raise DataStoreWriteError(f"Unable to write to data store '{self.bpmn_id}' using location '{location}'.")
|
||||||
data=data,
|
|
||||||
)
|
data = my_task.data[self.bpmn_id]
|
||||||
|
|
||||||
|
# TODO: validate data against schema
|
||||||
|
model.data = data
|
||||||
|
|
||||||
db.session.query(JSONDataStoreModel).filter_by(name=self.bpmn_id, location=location).delete()
|
|
||||||
db.session.add(model)
|
db.session.add(model)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
del my_task.data[self.bpmn_id]
|
del my_task.data[self.bpmn_id]
|
||||||
|
|
||||||
|
def _data_store_location_for_task(self, spiff_task: SpiffTask, identifier: str) -> str | None:
|
||||||
|
location = _process_model_location_for_task(spiff_task)
|
||||||
|
if location is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
locations = UpsearchService.upsearch_locations(location)
|
||||||
|
model = (
|
||||||
|
JSONDataStoreModel.query.filter_by(identifier=identifier)
|
||||||
|
.filter(JSONDataStoreModel.location.in_(locations)) # type: ignore
|
||||||
|
.order_by(JSONDataStoreModel.location.desc()) # type: ignore
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if model is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return model.location # type: ignore
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def register_data_store_class(data_store_classes: dict[str, Any]) -> None:
|
def register_data_store_class(data_store_classes: dict[str, Any]) -> None:
|
||||||
data_store_classes["JSONDataStore"] = JSONDataStore
|
data_store_classes["JSONDataStore"] = JSONDataStore
|
||||||
|
@ -114,21 +136,40 @@ class JSONFileDataStore(BpmnDataStoreSpecification): # type: ignore
|
||||||
|
|
||||||
def get(self, my_task: SpiffTask) -> None:
|
def get(self, my_task: SpiffTask) -> None:
|
||||||
"""get."""
|
"""get."""
|
||||||
location = _data_store_location_for_task(my_task, self.bpmn_id)
|
location = self._data_store_location_for_task(my_task, self.bpmn_id)
|
||||||
if location is None:
|
if location is None:
|
||||||
raise Exception(f"Unable to read from data store '{self.bpmn_id}' using location '{location}'.")
|
raise DataStoreReadError(f"Unable to read from data store '{self.bpmn_id}' using location '{location}'.")
|
||||||
contents = FileSystemService.contents_of_json_file_at_relative_path(location, _data_store_filename(self.bpmn_id))
|
contents = FileSystemService.contents_of_json_file_at_relative_path(location, self._data_store_filename(self.bpmn_id))
|
||||||
my_task.data[self.bpmn_id] = contents
|
my_task.data[self.bpmn_id] = contents
|
||||||
|
|
||||||
def set(self, my_task: SpiffTask) -> None:
|
def set(self, my_task: SpiffTask) -> None:
|
||||||
"""set."""
|
"""set."""
|
||||||
location = _data_store_location_for_task(my_task, self.bpmn_id)
|
location = self._data_store_location_for_task(my_task, self.bpmn_id)
|
||||||
if location is None:
|
if location is None:
|
||||||
raise Exception(f"Unable to write to data store '{self.bpmn_id}' using location '{location}'.")
|
raise DataStoreWriteError(f"Unable to write to data store '{self.bpmn_id}' using location '{location}'.")
|
||||||
data = my_task.data[self.bpmn_id]
|
data = my_task.data[self.bpmn_id]
|
||||||
FileSystemService.write_to_json_file_at_relative_path(location, _data_store_filename(self.bpmn_id), data)
|
FileSystemService.write_to_json_file_at_relative_path(location, self._data_store_filename(self.bpmn_id), data)
|
||||||
del my_task.data[self.bpmn_id]
|
del my_task.data[self.bpmn_id]
|
||||||
|
|
||||||
|
def _data_store_location_for_task(self, spiff_task: SpiffTask, identifier: str) -> str | None:
|
||||||
|
location = _process_model_location_for_task(spiff_task)
|
||||||
|
if location is None:
|
||||||
|
return None
|
||||||
|
if self._data_store_exists_at_location(location, identifier):
|
||||||
|
return location
|
||||||
|
location = ReferenceCacheService.upsearch(location, identifier, "data_store")
|
||||||
|
if location is None:
|
||||||
|
return None
|
||||||
|
if not self._data_store_exists_at_location(location, identifier):
|
||||||
|
return None
|
||||||
|
return location
|
||||||
|
|
||||||
|
def _data_store_exists_at_location(self, location: str, identifier: str) -> bool:
|
||||||
|
return FileSystemService.file_exists_at_relative_path(location, self._data_store_filename(identifier))
|
||||||
|
|
||||||
|
def _data_store_filename(self, name: str) -> str:
|
||||||
|
return f"{name}.json"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def register_data_store_class(data_store_classes: dict[str, Any]) -> None:
|
def register_data_store_class(data_store_classes: dict[str, Any]) -> None:
|
||||||
data_store_classes["JSONFileDataStore"] = JSONFileDataStore
|
data_store_classes["JSONFileDataStore"] = JSONFileDataStore
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from sqlalchemy import UniqueConstraint
|
||||||
|
|
||||||
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
|
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
|
||||||
from spiffworkflow_backend.models.db import db
|
from spiffworkflow_backend.models.db import db
|
||||||
|
|
||||||
|
@ -7,10 +9,14 @@ from spiffworkflow_backend.models.db import db
|
||||||
@dataclass
|
@dataclass
|
||||||
class JSONDataStoreModel(SpiffworkflowBaseDBModel):
|
class JSONDataStoreModel(SpiffworkflowBaseDBModel):
|
||||||
__tablename__ = "json_data_store"
|
__tablename__ = "json_data_store"
|
||||||
|
__table_args__ = (UniqueConstraint("identifier", "location", name="_identifier_location_unique"),)
|
||||||
|
|
||||||
id: int = db.Column(db.Integer, primary_key=True)
|
id: int = db.Column(db.Integer, primary_key=True)
|
||||||
name: str = db.Column(db.String(255), index=True)
|
name: str = db.Column(db.String(255), index=True, nullable=False)
|
||||||
location: str = db.Column(db.String(255))
|
identifier: str = db.Column(db.String(255), index=True, nullable=False)
|
||||||
data: dict = db.Column(db.JSON)
|
location: str = db.Column(db.String(255), nullable=False)
|
||||||
updated_at_in_seconds: int = db.Column(db.Integer)
|
schema: dict = db.Column(db.JSON, nullable=False)
|
||||||
created_at_in_seconds: int = db.Column(db.Integer)
|
data: dict = db.Column(db.JSON, nullable=False)
|
||||||
|
description: str = db.Column(db.String(255))
|
||||||
|
updated_at_in_seconds: int = db.Column(db.Integer, nullable=False)
|
||||||
|
created_at_in_seconds: int = db.Column(db.Integer, nullable=False)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""APIs for dealing with process groups, process models, and process instances."""
|
"""APIs for dealing with process groups, process models, and process instances."""
|
||||||
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import flask.wrappers
|
import flask.wrappers
|
||||||
|
@ -11,6 +11,12 @@ from spiffworkflow_backend.data_stores.kkv import KKVDataStore
|
||||||
from spiffworkflow_backend.data_stores.typeahead import TypeaheadDataStore
|
from spiffworkflow_backend.data_stores.typeahead import TypeaheadDataStore
|
||||||
from spiffworkflow_backend.exceptions.api_error import ApiError
|
from spiffworkflow_backend.exceptions.api_error import ApiError
|
||||||
|
|
||||||
|
DATA_STORES = {
|
||||||
|
"json": (JSONDataStore, "JSON Data Store"),
|
||||||
|
"kkv": (KKVDataStore, "Keyed Key-Value Data Store"),
|
||||||
|
"typeahead": (TypeaheadDataStore, "Typeahead Data Store"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def data_store_list() -> flask.wrappers.Response:
|
def data_store_list() -> flask.wrappers.Response:
|
||||||
"""Returns a list of the names of all the data stores."""
|
"""Returns a list of the names of all the data stores."""
|
||||||
|
@ -25,6 +31,16 @@ def data_store_list() -> flask.wrappers.Response:
|
||||||
return make_response(jsonify(data_stores), 200)
|
return make_response(jsonify(data_stores), 200)
|
||||||
|
|
||||||
|
|
||||||
|
def data_store_types() -> flask.wrappers.Response:
|
||||||
|
"""Returns a list of the types of available data stores."""
|
||||||
|
|
||||||
|
# this if == "json" check is temporary while we roll out support for other data stores
|
||||||
|
# being created with locations, identifiers and schemas
|
||||||
|
data_store_types = [{"type": k, "name": v[0].__name__, "description": v[1]} for k, v in DATA_STORES.items() if k == "json"]
|
||||||
|
|
||||||
|
return make_response(jsonify(data_store_types), 200)
|
||||||
|
|
||||||
|
|
||||||
def _build_response(data_store_class: Any, name: str, page: int, per_page: int) -> 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.query_data_store(name)
|
||||||
data = data_store_query.paginate(page=page, per_page=per_page, error_out=False)
|
data = data_store_query.paginate(page=page, per_page=per_page, error_out=False)
|
||||||
|
@ -46,13 +62,41 @@ def _build_response(data_store_class: Any, name: str, page: int, per_page: int)
|
||||||
def data_store_item_list(data_store_type: str, name: str, page: int = 1, per_page: int = 100) -> flask.wrappers.Response:
|
def data_store_item_list(data_store_type: str, name: str, page: int = 1, per_page: int = 100) -> flask.wrappers.Response:
|
||||||
"""Returns a list of the items in a data store."""
|
"""Returns a list of the items in a data store."""
|
||||||
|
|
||||||
if data_store_type == "typeahead":
|
if data_store_type not in DATA_STORES:
|
||||||
return _build_response(TypeaheadDataStore, name, page, per_page)
|
raise ApiError("unknown_data_store", f"Unknown data store type: {data_store_type}", status_code=400)
|
||||||
|
|
||||||
if data_store_type == "kkv":
|
data_store_class, _ = DATA_STORES[data_store_type]
|
||||||
return _build_response(KKVDataStore, name, page, per_page)
|
return _build_response(data_store_class, name, page, per_page)
|
||||||
|
|
||||||
if data_store_type == "json":
|
|
||||||
return _build_response(JSONDataStore, name, page, per_page)
|
|
||||||
|
|
||||||
raise ApiError("unknown_data_store", f"Unknown data store type: {data_store_type}", status_code=400)
|
def data_store_create(body: dict) -> flask.wrappers.Response:
|
||||||
|
try:
|
||||||
|
data_store_type = body["type"]
|
||||||
|
name = body["name"]
|
||||||
|
identifier = body["id"]
|
||||||
|
location = body["location"]
|
||||||
|
description = body.get("description")
|
||||||
|
schema = body["schema"]
|
||||||
|
except Exception as e:
|
||||||
|
raise ApiError(
|
||||||
|
"data_store_required_key_missing",
|
||||||
|
"A valid JSON Schema is required when creating a new data store instance.",
|
||||||
|
status_code=400,
|
||||||
|
) from e
|
||||||
|
|
||||||
|
try:
|
||||||
|
schema = json.loads(schema)
|
||||||
|
except Exception as e:
|
||||||
|
raise ApiError(
|
||||||
|
"data_store_invalid_schema",
|
||||||
|
"A valid JSON Schema is required when creating a new data store instance.",
|
||||||
|
status_code=400,
|
||||||
|
) from e
|
||||||
|
|
||||||
|
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_class.create_instance(name, identifier, location, schema, description)
|
||||||
|
|
||||||
|
return make_response(jsonify({"ok": True}), 200)
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import os
|
|
||||||
|
|
||||||
from sqlalchemy import insert
|
from sqlalchemy import insert
|
||||||
|
|
||||||
from spiffworkflow_backend.models.cache_generation import CacheGenerationModel
|
from spiffworkflow_backend.models.cache_generation import CacheGenerationModel
|
||||||
from spiffworkflow_backend.models.db import db
|
from spiffworkflow_backend.models.db import db
|
||||||
from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
|
from spiffworkflow_backend.models.reference_cache import ReferenceCacheModel
|
||||||
|
from spiffworkflow_backend.services.upsearch_service import UpsearchService
|
||||||
|
|
||||||
|
|
||||||
class ReferenceCacheService:
|
class ReferenceCacheService:
|
||||||
|
@ -37,7 +36,7 @@ class ReferenceCacheService:
|
||||||
cache_generation = CacheGenerationModel.newest_generation_for_table("reference_cache")
|
cache_generation = CacheGenerationModel.newest_generation_for_table("reference_cache")
|
||||||
if cache_generation is None:
|
if cache_generation is None:
|
||||||
return None
|
return None
|
||||||
locations = cls.upsearch_locations(location)
|
locations = UpsearchService.upsearch_locations(location)
|
||||||
references = (
|
references = (
|
||||||
ReferenceCacheModel.query.filter_by(
|
ReferenceCacheModel.query.filter_by(
|
||||||
identifier=identifier,
|
identifier=identifier,
|
||||||
|
@ -54,13 +53,3 @@ class ReferenceCacheService:
|
||||||
return reference.relative_location # type: ignore
|
return reference.relative_location # type: ignore
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def upsearch_locations(cls, location: str) -> list[str]:
|
|
||||||
locations = []
|
|
||||||
|
|
||||||
while location != "":
|
|
||||||
locations.append(location)
|
|
||||||
location = os.path.dirname(location)
|
|
||||||
|
|
||||||
return locations
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class UpsearchService:
|
||||||
|
@classmethod
|
||||||
|
def upsearch_locations(cls, process_model_identifier: str) -> list[str]:
|
||||||
|
location = process_model_identifier
|
||||||
|
locations = []
|
||||||
|
|
||||||
|
while location != "":
|
||||||
|
locations.append(location)
|
||||||
|
location = os.path.dirname(location)
|
||||||
|
|
||||||
|
return locations
|
|
@ -41,17 +41,6 @@ def with_loaded_reference_cache(app: Flask, with_db_and_bpmn_file_cleanup: None)
|
||||||
|
|
||||||
|
|
||||||
class TestReferenceCacheService(BaseTest):
|
class TestReferenceCacheService(BaseTest):
|
||||||
def test_upsearch_locations(
|
|
||||||
self,
|
|
||||||
) -> None:
|
|
||||||
locations = ReferenceCacheService.upsearch_locations("misc/jonjon/generic-data-store-area/test-level-2")
|
|
||||||
assert locations == [
|
|
||||||
"misc/jonjon/generic-data-store-area/test-level-2",
|
|
||||||
"misc/jonjon/generic-data-store-area",
|
|
||||||
"misc/jonjon",
|
|
||||||
"misc",
|
|
||||||
]
|
|
||||||
|
|
||||||
def test_can_find_data_store_in_current_location(self, with_loaded_reference_cache: None) -> None:
|
def test_can_find_data_store_in_current_location(self, with_loaded_reference_cache: None) -> None:
|
||||||
location = ReferenceCacheService.upsearch(
|
location = ReferenceCacheService.upsearch(
|
||||||
"misc/jonjon/generic-data-store-area/test-level-1", "contacts_datastore", "data_store"
|
"misc/jonjon/generic-data-store-area/test-level-1", "contacts_datastore", "data_store"
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
from spiffworkflow_backend.services.upsearch_service import UpsearchService
|
||||||
|
|
||||||
|
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||||
|
|
||||||
|
|
||||||
|
class TestUpsearchService(BaseTest):
|
||||||
|
def test_upsearch_locations(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
locations = UpsearchService.upsearch_locations("misc/jonjon/generic-data-store-area/test-level-2")
|
||||||
|
assert locations == [
|
||||||
|
"misc/jonjon/generic-data-store-area/test-level-2",
|
||||||
|
"misc/jonjon/generic-data-store-area",
|
||||||
|
"misc/jonjon",
|
||||||
|
"misc",
|
||||||
|
]
|
|
@ -0,0 +1,263 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
// @ts-ignore
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ComboBox,
|
||||||
|
Form,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
TextArea,
|
||||||
|
} from '@carbon/react';
|
||||||
|
import HttpService from '../services/HttpService';
|
||||||
|
import { DataStore, DataStoreType } from '../interfaces';
|
||||||
|
import {
|
||||||
|
modifyProcessIdentifierForPathParam,
|
||||||
|
truncateString,
|
||||||
|
} from '../helpers';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
mode: string;
|
||||||
|
dataStore: DataStore;
|
||||||
|
setDataStore: (..._args: any[]) => any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DataStoreForm({
|
||||||
|
mode,
|
||||||
|
dataStore,
|
||||||
|
setDataStore,
|
||||||
|
}: OwnProps) {
|
||||||
|
const [identifierInvalid, setIdentifierInvalid] = useState<boolean>(false);
|
||||||
|
const [idHasBeenUpdatedByUser, setIdHasBeenUpdatedByUser] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const [nameInvalid, setNameInvalid] = useState<boolean>(false);
|
||||||
|
const [typeInvalid, setTypeInvalid] = useState<boolean>(false);
|
||||||
|
const [schemaInvalid, setSchemaInvalid] = useState<boolean>(false);
|
||||||
|
const [dataStoreTypes, setDataStoreTypes] = useState<[DataStoreType] | []>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const [selectedDataStoreType, setSelectedDataStoreType] =
|
||||||
|
useState<DataStoreType | null>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const dataStoreLocation = () => {
|
||||||
|
const searchParams = new URLSearchParams(document.location.search);
|
||||||
|
const parentGroupId = searchParams.get('parentGroupId');
|
||||||
|
|
||||||
|
return parentGroupId ?? '/';
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleSetDataStoreTypesCallback = (result: any) => {
|
||||||
|
setDataStoreTypes(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
HttpService.makeCallToBackend({
|
||||||
|
path: '/data-stores/types',
|
||||||
|
successCallback: handleSetDataStoreTypesCallback,
|
||||||
|
httpMethod: 'GET',
|
||||||
|
});
|
||||||
|
}, [setDataStoreTypes]);
|
||||||
|
|
||||||
|
const navigateToDataStores = (_result: any) => {
|
||||||
|
const location = dataStoreLocation();
|
||||||
|
if (location !== '/') {
|
||||||
|
navigate(
|
||||||
|
`/process-groups/${modifyProcessIdentifierForPathParam(location)}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
navigate(`/process-groups`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasValidIdentifier = (identifierToCheck: string) => {
|
||||||
|
return identifierToCheck.match(/^[a-z][0-9a-z_]*[a-z0-9]$/);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmission = (event: any) => {
|
||||||
|
const searchParams = new URLSearchParams(document.location.search);
|
||||||
|
const parentGroupId = searchParams.get('parentGroupId');
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
let hasErrors = false;
|
||||||
|
if (mode === 'new' && !hasValidIdentifier(dataStore.id)) {
|
||||||
|
setIdentifierInvalid(true);
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
if (dataStore.name === '') {
|
||||||
|
setNameInvalid(true);
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
if (selectedDataStoreType === null) {
|
||||||
|
setTypeInvalid(true);
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
if (dataStore.schema === '') {
|
||||||
|
setSchemaInvalid(true);
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
if (hasErrors) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let path = '/data-stores';
|
||||||
|
let httpMethod = 'POST';
|
||||||
|
if (mode === 'edit') {
|
||||||
|
path = `/data-stores/${dataStore.id}`;
|
||||||
|
httpMethod = 'PUT';
|
||||||
|
}
|
||||||
|
const postBody = {
|
||||||
|
id: dataStore.id,
|
||||||
|
name: dataStore.name,
|
||||||
|
description: dataStore.description,
|
||||||
|
type: dataStore.type,
|
||||||
|
schema: dataStore.schema,
|
||||||
|
location: parentGroupId ?? '/',
|
||||||
|
};
|
||||||
|
|
||||||
|
HttpService.makeCallToBackend({
|
||||||
|
path,
|
||||||
|
successCallback: navigateToDataStores,
|
||||||
|
httpMethod,
|
||||||
|
postBody,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDataStore = (newValues: any) => {
|
||||||
|
const dataStoreToCopy = {
|
||||||
|
...dataStore,
|
||||||
|
};
|
||||||
|
Object.assign(dataStoreToCopy, newValues);
|
||||||
|
setDataStore(dataStoreToCopy);
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeIdentifier = (str: any) => {
|
||||||
|
return str
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/[^\w\s-]/g, '')
|
||||||
|
.replace(/[\s-]+/g, '_')
|
||||||
|
.replace(/^[-\d]+/g, '')
|
||||||
|
.replace(/-+$/g, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onNameChanged = (newName: any) => {
|
||||||
|
setNameInvalid(false);
|
||||||
|
const updateDict = { name: newName };
|
||||||
|
if (!idHasBeenUpdatedByUser && mode === 'new') {
|
||||||
|
Object.assign(updateDict, { id: makeIdentifier(newName) });
|
||||||
|
}
|
||||||
|
updateDataStore(updateDict);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTypeChanged = (newType: any) => {
|
||||||
|
setTypeInvalid(false);
|
||||||
|
const newTypeSelection = newType.selectedItem;
|
||||||
|
const updateDict = { type: newTypeSelection.type };
|
||||||
|
updateDataStore(updateDict);
|
||||||
|
setSelectedDataStoreType(newTypeSelection);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSchemaChanged = (newSchema: any) => {
|
||||||
|
setSchemaInvalid(false);
|
||||||
|
const updateDict = { schema: newSchema };
|
||||||
|
updateDataStore(updateDict);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formElements = () => {
|
||||||
|
const textInputs = [
|
||||||
|
<TextInput
|
||||||
|
id="data-store-name"
|
||||||
|
data-qa="data-store-name-input"
|
||||||
|
name="name"
|
||||||
|
invalidText="Name is required."
|
||||||
|
invalid={nameInvalid}
|
||||||
|
labelText="Name*"
|
||||||
|
value={dataStore.name}
|
||||||
|
onChange={(event: any) => onNameChanged(event.target.value)}
|
||||||
|
/>,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (mode === 'new') {
|
||||||
|
textInputs.push(
|
||||||
|
<TextInput
|
||||||
|
id="data-store-identifier"
|
||||||
|
name="id"
|
||||||
|
invalidText="Identifier is required and must be all lowercase characters and hyphens."
|
||||||
|
invalid={identifierInvalid}
|
||||||
|
labelText="Identifier*"
|
||||||
|
value={dataStore.id}
|
||||||
|
onChange={(event: any) => {
|
||||||
|
updateDataStore({ id: event.target.value });
|
||||||
|
// was invalid, and now valid
|
||||||
|
if (identifierInvalid && hasValidIdentifier(event.target.value)) {
|
||||||
|
setIdentifierInvalid(false);
|
||||||
|
}
|
||||||
|
setIdHasBeenUpdatedByUser(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
textInputs.push(
|
||||||
|
<ComboBox
|
||||||
|
onChange={onTypeChanged}
|
||||||
|
id="data-store-type-select"
|
||||||
|
data-qa="data-store-type-selection"
|
||||||
|
items={dataStoreTypes}
|
||||||
|
itemToString={(dataStoreType: DataStoreType) => {
|
||||||
|
if (dataStoreType) {
|
||||||
|
return `${dataStoreType.name} (${truncateString(
|
||||||
|
dataStoreType.description,
|
||||||
|
75
|
||||||
|
)})`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
titleText="Type*"
|
||||||
|
invalidText="Type is required."
|
||||||
|
invalid={typeInvalid}
|
||||||
|
placeholder="Choose the data store type"
|
||||||
|
selectedItem={selectedDataStoreType}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
textInputs.push(
|
||||||
|
<TextArea
|
||||||
|
id="data-store-schema"
|
||||||
|
name="schema"
|
||||||
|
invalidText="Schema is required and must be valid JSON."
|
||||||
|
invalid={schemaInvalid}
|
||||||
|
labelText="Schema"
|
||||||
|
value={dataStore.schema}
|
||||||
|
onChange={(event: any) => onSchemaChanged(event.target.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
textInputs.push(
|
||||||
|
<TextArea
|
||||||
|
id="data-store-description"
|
||||||
|
name="description"
|
||||||
|
labelText="Description"
|
||||||
|
value={dataStore.description}
|
||||||
|
onChange={(event: any) =>
|
||||||
|
updateDataStore({ description: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return textInputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formButtons = () => {
|
||||||
|
return <Button type="submit">Submit</Button>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleFormSubmission}>
|
||||||
|
<Stack gap={5}>
|
||||||
|
{formElements()}
|
||||||
|
{formButtons()}
|
||||||
|
</Stack>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Dropdown,
|
||||||
|
Table,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@carbon/react';
|
||||||
|
import { TableBody, TableCell } from '@mui/material';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import HttpService from '../services/HttpService';
|
||||||
|
import { DataStore, DataStoreRecords, PaginationObject } from '../interfaces';
|
||||||
|
import PaginationForTable from './PaginationForTable';
|
||||||
|
import { getPageInfoFromSearchParams } from '../helpers';
|
||||||
|
|
||||||
|
export default function DataStoreListTable() {
|
||||||
|
const [dataStores, setDataStores] = useState<DataStore[]>([]);
|
||||||
|
const [dataStore, setDataStore] = useState<DataStore | null>(null);
|
||||||
|
const [pagination, setPagination] = useState<PaginationObject | null>(null);
|
||||||
|
const [results, setResults] = useState<any[]>([]);
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
HttpService.makeCallToBackend({
|
||||||
|
path: `/data-stores`,
|
||||||
|
successCallback: (newStores: DataStore[]) => {
|
||||||
|
setDataStores(newStores);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, []); // Do this once so we have a list of data stores to select from.
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { page, perPage } = getPageInfoFromSearchParams(
|
||||||
|
searchParams,
|
||||||
|
10,
|
||||||
|
1,
|
||||||
|
'datastore'
|
||||||
|
);
|
||||||
|
const dataStoreType = searchParams.get('type') || '';
|
||||||
|
const dataStoreName = searchParams.get('name') || '';
|
||||||
|
|
||||||
|
if (dataStoreType === '' || dataStoreName === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dataStores && dataStoreName && dataStoreType) {
|
||||||
|
dataStores.forEach((ds) => {
|
||||||
|
if (ds.name === dataStoreName && ds.type === dataStoreType) {
|
||||||
|
setDataStore(ds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const queryParamString = `per_page=${perPage}&page=${page}`;
|
||||||
|
HttpService.makeCallToBackend({
|
||||||
|
path: `/data-stores/${dataStoreType}/${dataStoreName}?${queryParamString}`,
|
||||||
|
successCallback: (response: DataStoreRecords) => {
|
||||||
|
setResults(response.results);
|
||||||
|
setPagination(response.pagination);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [dataStores, searchParams]);
|
||||||
|
|
||||||
|
const getCell = (value: any) => {
|
||||||
|
const valueToUse =
|
||||||
|
typeof value === 'object' ? (
|
||||||
|
<pre>
|
||||||
|
<code>{JSON.stringify(value, null, 4)}</code>
|
||||||
|
</pre>
|
||||||
|
) : (
|
||||||
|
value
|
||||||
|
);
|
||||||
|
|
||||||
|
return <TableCell>{valueToUse}</TableCell>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTable = () => {
|
||||||
|
if (results.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const firstResult = results[0];
|
||||||
|
const tableHeaders: any[] = [];
|
||||||
|
const keys = Object.keys(firstResult);
|
||||||
|
keys.forEach((key) => tableHeaders.push(<TableHeader>{key}</TableHeader>));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table striped bordered>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>{tableHeaders}</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{results.map((object) => {
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
{keys.map((key) => {
|
||||||
|
return getCell(object[key]);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { page, perPage } = getPageInfoFromSearchParams(
|
||||||
|
searchParams,
|
||||||
|
10,
|
||||||
|
1,
|
||||||
|
'datastore'
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dropdown
|
||||||
|
id="data-store-dropdown"
|
||||||
|
titleText="Select Data Store"
|
||||||
|
helperText="Select the data store you wish to view"
|
||||||
|
label="Please select a data store"
|
||||||
|
items={dataStores}
|
||||||
|
selectedItem={dataStore}
|
||||||
|
itemToString={(ds: DataStore) => (ds ? `${ds.name} (${ds.type})` : '')}
|
||||||
|
onChange={(event: any) => {
|
||||||
|
setDataStore(event.selectedItem);
|
||||||
|
searchParams.set('datastore_page', '1');
|
||||||
|
searchParams.set('datastore_per_page', '10');
|
||||||
|
searchParams.set('type', event.selectedItem.type);
|
||||||
|
searchParams.set('name', event.selectedItem.name);
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<PaginationForTable
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
pagination={pagination}
|
||||||
|
tableToDisplay={getTable()}
|
||||||
|
paginationQueryParamPrefix="datastore"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -460,6 +460,15 @@ export interface DataStoreRecords {
|
||||||
export interface DataStore {
|
export interface DataStore {
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
id: string;
|
||||||
|
schema: string;
|
||||||
|
description?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataStoreType {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JsonSchemaExample {
|
export interface JsonSchemaExample {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Route, Routes } from 'react-router-dom';
|
import { Route, Routes } from 'react-router-dom';
|
||||||
import Configuration from './Configuration';
|
import Configuration from './Configuration';
|
||||||
import MessageListPage from './MessageListPage';
|
import MessageListPage from './MessageListPage';
|
||||||
import DataStorePage from './DataStorePage';
|
import DataStoreRoutes from './DataStoreRoutes';
|
||||||
import { UiSchemaUxElement } from '../extension_ui_schema_interfaces';
|
import { UiSchemaUxElement } from '../extension_ui_schema_interfaces';
|
||||||
import HomeRoutes from './HomeRoutes';
|
import HomeRoutes from './HomeRoutes';
|
||||||
import ProcessGroupRoutes from './ProcessGroupRoutes';
|
import ProcessGroupRoutes from './ProcessGroupRoutes';
|
||||||
|
@ -39,7 +39,7 @@ export default function BaseRoutes({ extensionUxElements }: OwnProps) {
|
||||||
element={<Configuration extensionUxElements={extensionUxElements} />}
|
element={<Configuration extensionUxElements={extensionUxElements} />}
|
||||||
/>
|
/>
|
||||||
<Route path="messages" element={<MessageListPage />} />
|
<Route path="messages" element={<MessageListPage />} />
|
||||||
<Route path="data-stores" element={<DataStorePage />} />
|
<Route path="data-stores/*" element={<DataStoreRoutes />} />
|
||||||
<Route path="about" element={<About />} />
|
<Route path="about" element={<About />} />
|
||||||
<Route path="admin/*" element={<AdminRedirect />} />
|
<Route path="admin/*" element={<AdminRedirect />} />
|
||||||
<Route path="/*" element={<Page404 />} />
|
<Route path="/*" element={<Page404 />} />
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DataStoreList from '../components/DataStoreList';
|
import DataStoreListTable from '../components/DataStoreListTable';
|
||||||
import { setPageTitle } from '../helpers';
|
import { setPageTitle } from '../helpers';
|
||||||
|
|
||||||
export default function DataStorePage() {
|
export default function DataStoreList() {
|
||||||
setPageTitle(['Data Stores']);
|
setPageTitle(['Data Stores']);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>Data Stores</h1>
|
<h1>Data Stores</h1>
|
||||||
<DataStoreList />
|
<DataStoreListTable />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||||
|
import DataStoreForm from '../components/DataStoreForm';
|
||||||
|
import { DataStore, HotCrumbItem } from '../interfaces';
|
||||||
|
import { setPageTitle } from '../helpers';
|
||||||
|
|
||||||
|
export default function DataStoreNew() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const parentGroupId = searchParams.get('parentGroupId');
|
||||||
|
const [dataStore, setDataStore] = useState<DataStore>({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
schema: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
setPageTitle(['New Data Store']);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hotCrumbs: HotCrumbItem[] = [['Process Groups', '/process-groups']];
|
||||||
|
if (parentGroupId) {
|
||||||
|
hotCrumbs.push({
|
||||||
|
entityToExplode: parentGroupId,
|
||||||
|
entityType: 'process-group-id',
|
||||||
|
linkLastItem: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ProcessBreadcrumb hotCrumbs={hotCrumbs} />
|
||||||
|
<h1>Add Data Store</h1>
|
||||||
|
<DataStoreForm
|
||||||
|
mode="new"
|
||||||
|
dataStore={dataStore}
|
||||||
|
setDataStore={setDataStore}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Route, Routes } from 'react-router-dom';
|
||||||
|
import DataStoreList from './DataStoreList';
|
||||||
|
import DataStoreNew from './DataStoreNew';
|
||||||
|
|
||||||
|
export default function DataStoreRoutes() {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<DataStoreList />} />
|
||||||
|
<Route path="new" element={<DataStoreNew />} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ export default function ProcessGroupShow() {
|
||||||
|
|
||||||
const { targetUris } = useUriListForPermissions();
|
const { targetUris } = useUriListForPermissions();
|
||||||
const permissionRequestData: PermissionsToCheck = {
|
const permissionRequestData: PermissionsToCheck = {
|
||||||
|
[targetUris.dataStoreListPath]: ['POST'],
|
||||||
[targetUris.processGroupListPath]: ['POST'],
|
[targetUris.processGroupListPath]: ['POST'],
|
||||||
[targetUris.processGroupShowPath]: ['PUT', 'DELETE'],
|
[targetUris.processGroupShowPath]: ['PUT', 'DELETE'],
|
||||||
[targetUris.processModelCreatePath]: ['POST'],
|
[targetUris.processModelCreatePath]: ['POST'],
|
||||||
|
@ -131,6 +132,13 @@ export default function ProcessGroupShow() {
|
||||||
Add a process model
|
Add a process model
|
||||||
</Button>
|
</Button>
|
||||||
</Can>
|
</Can>
|
||||||
|
<Can I="POST" a={targetUris.dataStoreListPath} ability={ability}>
|
||||||
|
<Button
|
||||||
|
href={`/data-stores/new?parentGroupId=${processGroup.id}`}
|
||||||
|
>
|
||||||
|
Add a data store
|
||||||
|
</Button>
|
||||||
|
</Can>
|
||||||
</Stack>
|
</Stack>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
|
Loading…
Reference in New Issue