From 6d2bf9d510ea00304da298650dda795f7dca96ad Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 3 Jan 2025 16:03:02 -0500 Subject: [PATCH] updated data stores w/ burnettk --- .../a-spiffui-v3/components/DataStoreForm.tsx | 294 ++++++++++++++++++ .../components/DataStoreListTable.tsx | 164 ++++++++++ .../src/a-spiffui-v3/components/SideNav.tsx | 3 + .../src/a-spiffui-v3/views/DataStoreEdit.tsx | 59 ++++ .../src/a-spiffui-v3/views/DataStoreList.tsx | 14 + .../src/a-spiffui-v3/views/DataStoreNew.tsx | 57 ++++ .../a-spiffui-v3/views/DataStoreRoutes.tsx | 14 + .../src/routes/SpiffUIV3.tsx | 10 + 8 files changed, 615 insertions(+) create mode 100644 spiffworkflow-frontend/src/a-spiffui-v3/components/DataStoreForm.tsx create mode 100644 spiffworkflow-frontend/src/a-spiffui-v3/components/DataStoreListTable.tsx create mode 100644 spiffworkflow-frontend/src/a-spiffui-v3/views/DataStoreEdit.tsx create mode 100644 spiffworkflow-frontend/src/a-spiffui-v3/views/DataStoreList.tsx create mode 100644 spiffworkflow-frontend/src/a-spiffui-v3/views/DataStoreNew.tsx create mode 100644 spiffworkflow-frontend/src/a-spiffui-v3/views/DataStoreRoutes.tsx diff --git a/spiffworkflow-frontend/src/a-spiffui-v3/components/DataStoreForm.tsx b/spiffworkflow-frontend/src/a-spiffui-v3/components/DataStoreForm.tsx new file mode 100644 index 000000000..73947b2f5 --- /dev/null +++ b/spiffworkflow-frontend/src/a-spiffui-v3/components/DataStoreForm.tsx @@ -0,0 +1,294 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Button, + TextField, + Select, + MenuItem, + FormControl, + InputLabel, + TextareaAutosize, + Stack, +} from '@mui/material'; +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(false); + const [idHasBeenUpdatedByUser, setIdHasBeenUpdatedByUser] = + useState(false); + const [nameInvalid, setNameInvalid] = useState(false); + const [typeInvalid, setTypeInvalid] = useState(false); + const [schemaInvalid, setSchemaInvalid] = useState(false); + const [dataStoreTypes, setDataStoreTypes] = useState<[DataStoreType] | []>( + [], + ); + const [selectedDataStoreType, setSelectedDataStoreType] = + useState(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) => { + const dataStoreType = result.find((item: any) => { + return item.type === dataStore.type; + }); + setDataStoreTypes(result); + setSelectedDataStoreType(dataStoreType ?? null); + }; + + HttpService.makeCallToBackend({ + path: '/data-stores/types', + successCallback: handleSetDataStoreTypesCallback, + httpMethod: 'GET', + }); + }, [dataStore, 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; + } + const path = '/data-stores'; + let httpMethod = 'POST'; + if (mode === 'edit') { + 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 = (event: any) => { + setTypeInvalid(false); + const newTypeSelection = event.target.value; + if (newTypeSelection) { + const updateDict = { type: newTypeSelection }; + updateDataStore(updateDict); + } + setSelectedDataStoreType(newTypeSelection); + }; + + const onSchemaChanged = (newSchema: any) => { + setSchemaInvalid(false); + const updateDict = { schema: newSchema }; + updateDataStore(updateDict); + }; + + const dataStoreTypeDisplayString = ( + dataStoreType: DataStoreType | null, + ): string => { + if (dataStoreType) { + return `${dataStoreType.name} (${truncateString( + dataStoreType.description, + 75, + )})`; + } + return ''; + }; + + const formElements = () => { + const textInputs = [ + onNameChanged(event.target.value)} + />, + ]; + + textInputs.push( + { + updateDataStore({ id: event.target.value }); + if (identifierInvalid && hasValidIdentifier(event.target.value)) { + setIdentifierInvalid(false); + } + setIdHasBeenUpdatedByUser(true); + }} + />, + ); + + if (mode === 'edit') { + textInputs.push( + , + ); + } else { + textInputs.push( + + Type* + + + ); + } + + textInputs.push( + onSchemaChanged(event.target.value)} + />, + ); + + textInputs.push( + + updateDataStore({ description: event.target.value }) + } + />, + ); + + return textInputs; + }; + + const formButtons = () => { + return ; + }; + + return ( +
+ + {formElements()} + {formButtons()} + +
+ ); +} diff --git a/spiffworkflow-frontend/src/a-spiffui-v3/components/DataStoreListTable.tsx b/spiffworkflow-frontend/src/a-spiffui-v3/components/DataStoreListTable.tsx new file mode 100644 index 000000000..99ec642c5 --- /dev/null +++ b/spiffworkflow-frontend/src/a-spiffui-v3/components/DataStoreListTable.tsx @@ -0,0 +1,164 @@ +import { useEffect, useState } from 'react'; +import { + Table, + TableHead, + TableRow, + TableBody, + TableCell, + TableContainer, + TablePagination, + Paper, + MenuItem, + Select, + InputLabel, + FormControl, +} 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([]); + const [dataStore, setDataStore] = useState(null); + const [pagination, setPagination] = useState(null); + const [results, setResults] = useState([]); + 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 dataStoreIdentifier = searchParams.get('identifier') || ''; + const dataStoreLocation = searchParams.get('location') || ''; + + if (dataStoreType === '' || dataStoreIdentifier === '') { + return; + } + if (dataStores && dataStoreIdentifier && dataStoreType) { + dataStores.forEach((ds) => { + if ( + ds.id === dataStoreIdentifier && + ds.type === dataStoreType && + ds.location === dataStoreLocation + ) { + setDataStore(ds); + } + }); + } + const queryParamString = `per_page=${perPage}&page=${page}&location=${dataStoreLocation}`; + HttpService.makeCallToBackend({ + path: `/data-stores/${dataStoreType}/${dataStoreIdentifier}/items?${queryParamString}`, + successCallback: (response: DataStoreRecords) => { + setResults(response.results); + setPagination(response.pagination); + }, + }); + }, [dataStores, searchParams]); + + const getCell = (value: any) => { + const valueToUse = + typeof value === 'object' ? ( +
+          {JSON.stringify(value, null, 4)}
+        
+ ) : ( + value + ); + + return {valueToUse}; + }; + + 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({key})); + + return ( + + + + {tableHeaders} + + + {results.map((object) => { + return ( + + {keys.map((key) => { + return getCell(object[key]); + })} + + ); + })} + +
+
+ ); + }; + + const locationDescription = (ds: DataStore) => { + return ds.location ? ` @ ${ds.location}` : ''; + }; + + const { page, perPage } = getPageInfoFromSearchParams( + searchParams, + 10, + 1, + 'datastore', + ); + return ( + <> + + Select Data Store + + + + + ); +} diff --git a/spiffworkflow-frontend/src/a-spiffui-v3/components/SideNav.tsx b/spiffworkflow-frontend/src/a-spiffui-v3/components/SideNav.tsx index 6c2d234ef..f7b829f70 100644 --- a/spiffworkflow-frontend/src/a-spiffui-v3/components/SideNav.tsx +++ b/spiffworkflow-frontend/src/a-spiffui-v3/components/SideNav.tsx @@ -162,6 +162,7 @@ function SideNav({ {[ { text: 'HOME', icon: }, { text: 'START NEW PROCESS', icon: }, + { text: 'DATA STORES' }, ].map((item, index) => ( ({ + 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/${dataStoreType}/${dataStoreIdentifier}${queryParams}`, + successCallback: setDataStoreFromResult, + }); + }, [dataStoreIdentifier, parentGroupId, dataStoreType]); + + 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/a-spiffui-v3/views/DataStoreList.tsx b/spiffworkflow-frontend/src/a-spiffui-v3/views/DataStoreList.tsx new file mode 100644 index 000000000..e177493f5 --- /dev/null +++ b/spiffworkflow-frontend/src/a-spiffui-v3/views/DataStoreList.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import DataStoreListTable from '../components/DataStoreListTable'; +import { setPageTitle } from '../helpers'; +import { Typography } from '@mui/material'; + +export default function DataStoreList() { + setPageTitle(['Data Stores']); + return ( + <> + Data Stores + + + ); +} diff --git a/spiffworkflow-frontend/src/a-spiffui-v3/views/DataStoreNew.tsx b/spiffworkflow-frontend/src/a-spiffui-v3/views/DataStoreNew.tsx new file mode 100644 index 000000000..af6052945 --- /dev/null +++ b/spiffworkflow-frontend/src/a-spiffui-v3/views/DataStoreNew.tsx @@ -0,0 +1,57 @@ +import { useEffect, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import Typography from '@mui/material/Typography'; +import Link from '@mui/material/Link'; +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({ + 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 ( + <> + + {hotCrumbs.map((crumb, index) => ( + + {typeof crumb === 'string' ? crumb[0] : crumb.entityToExplode} + + ))} + + + Add Data Store + + + + ); +} diff --git a/spiffworkflow-frontend/src/a-spiffui-v3/views/DataStoreRoutes.tsx b/spiffworkflow-frontend/src/a-spiffui-v3/views/DataStoreRoutes.tsx new file mode 100644 index 000000000..6a58c6a9a --- /dev/null +++ b/spiffworkflow-frontend/src/a-spiffui-v3/views/DataStoreRoutes.tsx @@ -0,0 +1,14 @@ +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/SpiffUIV3.tsx b/spiffworkflow-frontend/src/routes/SpiffUIV3.tsx index e201e0add..3dae86956 100644 --- a/spiffworkflow-frontend/src/routes/SpiffUIV3.tsx +++ b/spiffworkflow-frontend/src/routes/SpiffUIV3.tsx @@ -29,6 +29,9 @@ import About from '../a-spiffui-v3/views/About'; import useAPIError from '../hooks/UseApiError'; import ComingSoon from '../components/ComingSoon'; import MessageListPage from '../a-spiffui-v3/views/MessageListPage'; +import DataStoreRoutes from '../a-spiffui-v3/views/DataStoreRoutes'; +import DataStoreNew from '../a-spiffui-v3/views/DataStoreNew'; +import DataStoreList from '../a-spiffui-v3/views/DataStoreList'; // Import the DataStoreList component const fadeIn = 'fadeIn'; const fadeOutImmediate = 'fadeOutImmediate'; @@ -249,6 +252,13 @@ export default function SpiffUIV3() { {/* element={} */} {/* /> */} } /> + } /> + } /> + } + />{' '} + {/* Add route for DataStoreList */}