updated data stores w/ burnettk

This commit is contained in:
jasquat 2025-01-03 16:03:02 -05:00
parent d18c8982b3
commit 6d2bf9d510
No known key found for this signature in database
8 changed files with 615 additions and 0 deletions

View File

@ -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<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) => {
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 = [
<TextField
id="data-store-name"
data-qa="data-store-name-input"
name="name"
error={nameInvalid}
helperText={nameInvalid ? "Name is required." : ""}
label="Name*"
value={dataStore.name}
onChange={(event: any) => onNameChanged(event.target.value)}
/>,
];
textInputs.push(
<TextField
id="data-store-identifier"
name="id"
InputProps={{
readOnly: mode === 'edit',
}}
error={identifierInvalid}
helperText={identifierInvalid ? "Identifier is required and must be all lowercase characters and hyphens." : ""}
label="Identifier*"
value={dataStore.id}
onChange={(event: any) => {
updateDataStore({ id: event.target.value });
if (identifierInvalid && hasValidIdentifier(event.target.value)) {
setIdentifierInvalid(false);
}
setIdHasBeenUpdatedByUser(true);
}}
/>,
);
if (mode === 'edit') {
textInputs.push(
<TextField
id="data-store-type"
name="data-store-type"
InputProps={{
readOnly: true,
}}
label="Type*"
value={dataStoreTypeDisplayString(selectedDataStoreType)}
/>,
);
} else {
textInputs.push(
<FormControl fullWidth error={typeInvalid}>
<InputLabel id="data-store-type-select-label">Type*</InputLabel>
<Select
labelId="data-store-type-select-label"
id="data-store-type-select"
value={selectedDataStoreType ? selectedDataStoreType.type : ''}
onChange={onTypeChanged}
label="Type*"
>
{dataStoreTypes.map((type) => (
<MenuItem key={type.type} value={type.type}>
{dataStoreTypeDisplayString(type)}
</MenuItem>
))}
</Select>
</FormControl>
);
}
textInputs.push(
<TextField
id="data-store-schema"
name="schema"
error={schemaInvalid}
helperText={schemaInvalid ? "Schema is required and must be valid JSON." : ""}
label="Schema*"
multiline
minRows={3}
value={dataStore.schema}
onChange={(event: any) => onSchemaChanged(event.target.value)}
/>,
);
textInputs.push(
<TextareaAutosize
id="data-store-description"
name="description"
aria-label="Description"
placeholder="Description"
value={dataStore.description}
onChange={(event: any) =>
updateDataStore({ description: event.target.value })
}
/>,
);
return textInputs;
};
const formButtons = () => {
return <Button type="submit" variant="contained">Submit</Button>;
};
return (
<form onSubmit={handleFormSubmission}>
<Stack spacing={2}>
{formElements()}
{formButtons()}
</Stack>
</form>
);
}

View File

@ -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<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 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' ? (
<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(<TableCell>{key}</TableCell>));
return (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>{tableHeaders}</TableRow>
</TableHead>
<TableBody>
{results.map((object) => {
return (
<TableRow>
{keys.map((key) => {
return getCell(object[key]);
})}
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
);
};
const locationDescription = (ds: DataStore) => {
return ds.location ? ` @ ${ds.location}` : '';
};
const { page, perPage } = getPageInfoFromSearchParams(
searchParams,
10,
1,
'datastore',
);
return (
<>
<FormControl fullWidth>
<InputLabel id="data-store-dropdown-label">Select Data Store</InputLabel>
<Select
labelId="data-store-dropdown-label"
id="data-store-dropdown"
value={dataStore ? dataStore.id : ''}
onChange={(event) => {
const selectedDataStore = dataStores.find(ds => ds.id === event.target.value);
if (selectedDataStore) {
setDataStore(selectedDataStore);
searchParams.set('datastore_page', '1');
searchParams.set('datastore_per_page', '10');
searchParams.set('type', selectedDataStore.type);
searchParams.set('identifier', selectedDataStore.id);
searchParams.set('location', selectedDataStore.location);
setSearchParams(searchParams);
}
}}
>
{dataStores.map((ds) => (
<MenuItem key={ds.id} value={ds.id}>
{`${ds.name} (${ds.type}${locationDescription(ds)})`}
</MenuItem>
))}
</Select>
</FormControl>
<PaginationForTable
page={page}
perPage={perPage}
pagination={pagination}
tableToDisplay={getTable()}
paginationQueryParamPrefix="datastore"
/>
</>
);
}

View File

@ -162,6 +162,7 @@ function SideNav({
{[
{ text: 'HOME', icon: <Home /> },
{ text: 'START NEW PROCESS', icon: <Add /> },
{ text: 'DATA STORES' },
].map((item, index) => (
<ListItem
button
@ -172,6 +173,8 @@ function SideNav({
navigate('/newui');
} else if (index === 1) {
navigate('/newui/startprocess');
} else if (item.text === 'DATA STORES') {
navigate('/newui/data-stores');
}
}}
sx={{

View File

@ -0,0 +1,59 @@
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 dataStoreType = searchParams.get('type');
const dataStoreIdentifier = params.data_store_identifier;
const [dataStore, setDataStore] = useState<DataStore>({
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 (
<>
<ProcessBreadcrumb hotCrumbs={hotCrumbs} />
<h1>Edit Data Store</h1>
<DataStoreForm
mode="edit"
dataStore={dataStore}
setDataStore={setDataStore}
/>
</>
);
}

View File

@ -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 (
<>
<Typography variant="h1">Data Stores</Typography>
<DataStoreListTable />
</>
);
}

View File

@ -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<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 (
<>
<Breadcrumbs aria-label="breadcrumb">
{hotCrumbs.map((crumb, index) => (
<Link
key={index}
color="inherit"
href={typeof crumb === 'string' ? crumb[1] : ''}
>
{typeof crumb === 'string' ? crumb[0] : crumb.entityToExplode}
</Link>
))}
</Breadcrumbs>
<Typography variant="h4" component="h1" gutterBottom>
Add Data Store
</Typography>
<DataStoreForm
mode="new"
dataStore={dataStore}
setDataStore={setDataStore}
/>
</>
);
}

View File

@ -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 (
<Routes>
<Route path="/" element={<DataStoreList />} />
<Route path=":data_store_identifier/edit" element={<DataStoreEdit />} />
<Route path="new" element={<DataStoreNew />} />
</Routes>
);
}

View File

@ -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={<ProcessInstanceProgressPage variant="all" />} */}
{/* /> */}
<Route path="/messages" element={<MessageListPage />} />
<Route path="/data-stores/*" element={<DataStoreRoutes />} />
<Route path="/data-store/new" element={<DataStoreNew />} />
<Route
path="/data-storelist"
element={<DataStoreList />}
/>{' '}
{/* Add route for DataStoreList */}
</Routes>
</Box>
</Box>