Skip to content

Commit

Permalink
Merge branch 'SIMSBIOHUB-397' into dataset_security_feature
Browse files Browse the repository at this point in the history
  • Loading branch information
al-rosenthal committed Dec 12, 2023
2 parents 6a238f3 + ebcb1f1 commit 6b9c797
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 132 deletions.
42 changes: 15 additions & 27 deletions api/src/paths/administrative/security/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,37 +19,17 @@ export const GET: Operation = [
]
};
}),
() => {}
getActiveSecurityRules()
];

GET.apiDoc = {
description: 'Get all observations for the survey.',
tags: ['observation'],
description: 'Get all active security rules.',
tags: ['security'],
security: [
{
Bearer: []
}
],
parameters: [
{
in: 'path',
name: 'projectId',
schema: {
type: 'number',
minimum: 1
},
required: true
},
{
in: 'path',
name: 'surveyId',
schema: {
type: 'number',
minimum: 1
},
required: true
}
],
responses: {
200: {
description: 'Security Rules.',
Expand All @@ -73,7 +53,8 @@ GET.apiDoc = {
type: 'string'
},
record_end_date: {
type: 'string'
type: 'string',
nullable: true
},
create_date: {
type: 'string'
Expand All @@ -82,13 +63,15 @@ GET.apiDoc = {
type: 'number'
},
update_date: {
type: 'string'
type: 'string',
nullable: true
},
update_user: {
type: 'number'
type: 'number',
nullable: true
},
revision_count: {
type: 'string'
type: 'number'
}
}
}
Expand Down Expand Up @@ -120,7 +103,12 @@ export function getActiveSecurityRules(): RequestHandler {
const service = new SecurityService(connection);

try {
await connection.open();

const data = await service.getActiveSecurityRules();

await connection.commit();

return res.status(200).json(data);
} catch (error) {
defaultLog.error({ label: 'getActiveSecurityRules', message: 'error', error });
Expand Down
7 changes: 5 additions & 2 deletions api/src/repositories/security-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const SecurityRuleRecord = z.object({
name: z.string(),
description: z.string(),
record_effective_date: z.string(),
record_end_date: z.string(),
record_end_date: z.string().nullable(),
create_date: z.string(),
create_user: z.number(),
update_date: z.string().nullable(),
Expand Down Expand Up @@ -248,8 +248,11 @@ export class SecurityRepository extends BaseRepository {

async getActiveSecurityRules(): Promise<SecurityRuleRecord[]> {
defaultLog.debug({ label: 'getSecurityRules' });
const sql = SQL`SELECT * FROM security_rule WHERE record_end_date IS NULL;`;
const sql = SQL`
SELECT * FROM security_rule WHERE record_end_date IS NULL;
`;
const response = await this.connection.sql(sql, SecurityRuleRecord);
console.log(response.rows);
return response.rows;
}
}
13 changes: 10 additions & 3 deletions app/src/components/security/SecuritiesDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import EditDialog from 'components/dialog/EditDialog';
import { ISecurityRule } from 'hooks/api/useSecurityApi';
import yup from 'utils/YupSchema';
import SecurityRuleForm from './SecurityRuleForm';
interface ISecuritiesDialogProps {
isOpen: boolean;
onClose: () => void;
}

const SecuritiesDialog = (props: ISecuritiesDialogProps) => {
const SecurityRuleYupSchema = yup.array(yup.number());
export const SecurityRuleYupSchema = yup.object().shape({
rules: yup.array(yup.object()).min(1)
});

export interface ISecurityRuleFormProps {
rules: ISecurityRule[];
}

const SecuritiesDialog = (props: ISecuritiesDialogProps) => {
return (
<>
<EditDialog
Expand All @@ -19,7 +26,7 @@ const SecuritiesDialog = (props: ISecuritiesDialogProps) => {
onSave={() => console.log('SAVE SOME SECURITY RULES SON')}
component={{
element: <SecurityRuleForm />,
initialValues: [],
initialValues: { rules: [] },
validationSchema: SecurityRuleYupSchema
}}
/>
Expand Down
185 changes: 85 additions & 100 deletions app/src/components/security/SecurityRuleForm.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
import { mdiMagnify } from '@mdi/js';
import { mdiClose, mdiMagnify } from '@mdi/js';
import Icon from '@mdi/react';
import { Alert, AlertTitle, Typography } from '@mui/material';
import Autocomplete from '@mui/material/Autocomplete';
import { Alert, AlertTitle, IconButton, Paper, Typography } from '@mui/material';
import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete';
import Box from '@mui/material/Box';
import Collapse from '@mui/material/Collapse';
import TextField from '@mui/material/TextField';
import { useFormikContext } from 'formik';
import { FieldArray, FieldArrayRenderProps, useFormikContext } from 'formik';
import { ISecurityRule } from 'hooks/api/useSecurityApi';
import { useApi } from 'hooks/useApi';
import { useEffect, useState } from 'react';
import { TransitionGroup } from 'react-transition-group';
import { alphabetizeObjects } from 'utils/Utils';
import { ISecurityRuleFormProps } from './SecuritiesDialog';
import SecurityRuleCard from './SecurityRuleCard';

const SecurityRuleForm = () => {
const { handleSubmit, values, errors } = useFormikContext<any>();
const [selectedRules, setSelectedRules] = useState<any[]>([]);
const { handleSubmit, errors, values } = useFormikContext<ISecurityRuleFormProps>();
const [rules, setRules] = useState<ISecurityRule[]>([]);
const [searchText, setSearchText] = useState('');

const api = useApi();
console.log(values);
useEffect(() => {
const fetchData = async () => {
const data = await api.security.getActiveSecurityRules();
setRules(data);
setSelectedRules([]);
};

fetchData();
Expand All @@ -33,108 +29,97 @@ const SecurityRuleForm = () => {
return (
<form onSubmit={handleSubmit}>
<Box component="fieldset">
<Typography component="legend">Manage Team Members</Typography>
<Typography component="legend">Manage Security Rules</Typography>
<Typography
variant="body1"
color="textSecondary"
sx={{
maxWidth: '72ch'
}}>
A minimum of one team member must be assigned the coordinator role.
A minimum of one security rule must be selected.
</Typography>
{errors?.['participants'] && !selectedRules.length && (
{errors?.['rules'] && !values.rules.length && (
<Box mt={3}>
<Alert severity="error" variant="standard">
<AlertTitle>No Rules Selected</AlertTitle>
At least one team member needs to be added to this project.
At least one security rule needs to be selected.
</Alert>
</Box>
)}
{errors?.['participants'] && selectedRules.length > 0 && (
<Box mt={3}>{/* <AlertBar severity="error" variant="standard" title={''} text={''} /> */}</Box>
)}
<Box mt={3}>
<Autocomplete
id={'autocomplete-user-role-search'}
data-testid={'autocomplete-user-role-search'}
filterSelectedOptions
noOptionsText="No records found"
options={rules}
// filterOptions={(options, state) => {
// const searchFilter = createFilterOptions<ISystemUser>({ ignoreCase: true });
// const unselectedOptions = options.filter(
// (item) => !selectedUsers.some((existing) => existing.system_user_id === item.system_user_id)
// );
// return searchFilter(unselectedOptions, state);
// }}
getOptionLabel={(option) => option.name}
inputValue={searchText}
onInputChange={(_, value, reason) => {
if (reason === 'reset') {
setSearchText('');
} else {
setSearchText(value);
}
}}
// onChange={(_, option) => {
// if (option) {
// }
// }}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
placeholder={'Find Security Rules'}
fullWidth
InputProps={{
...params.InputProps,
startAdornment: (
<Box mx={1} mt="6px">
<Icon path={mdiMagnify} size={1}></Icon>
</Box>
)
}}
/>
)}
renderOption={(renderProps, renderOption) => {
return (
<Box component="li" {...renderProps}>
<SecurityRuleCard title={renderOption.name} subtitle={renderOption.description} />
</Box>
);
}}
/>
</Box>
<Box>
<Box
sx={{
'& .userRoleItemContainer + .userRoleItemContainer': {
mt: 1
}
}}>
<TransitionGroup>
{selectedRules.map((rule: any, index: number) => {
// const error = rowItemError(index);
return (
<Collapse>
{/* <UserRoleSelector
index={index}
user={user}
roles={props.roles}
error={error}
selectedRole={getSelectedRole(index)}
handleAdd={handleAddUserRole}
handleRemove={handleRemoveUser}
key={user.system_user_id}
label={'Select a Role'}
/> */}
<></>
</Collapse>
);
})}
</TransitionGroup>
</Box>
</Box>
<FieldArray name="rules">
{(helpers: FieldArrayRenderProps) => (
<>
<Box mt={3}>
<Autocomplete
id={'autocomplete-security-rule-search'}
data-testid={'autocomplete-security-rule-search'}
filterSelectedOptions
clearOnBlur
noOptionsText="No records found"
options={alphabetizeObjects(rules, 'name')}
filterOptions={(options, state) => {
const searchFilter = createFilterOptions<ISecurityRule>({ ignoreCase: true });
const unselectedOptions = options.filter(
(item) => !values.rules.some((existing) => existing.security_rule_id === item.security_rule_id)
);
return searchFilter(unselectedOptions, state);
}}
getOptionLabel={(option) => option.name}
isOptionEqualToValue={(option, value) => option.security_rule_id === value.security_rule_id}
onChange={(_, option) => {
if (option) {
helpers.push(option);
}
}}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
placeholder={'Find Security Rules'}
fullWidth
InputProps={{
...params.InputProps,
startAdornment: (
<Box mx={1} mt="6px">
<Icon path={mdiMagnify} size={1}></Icon>
</Box>
)
}}
/>
)}
renderOption={(renderProps, renderOption) => {
return (
<Box component="li" {...renderProps}>
<SecurityRuleCard title={renderOption.name} subtitle={renderOption.description} />
</Box>
);
}}
/>
</Box>
<Box mt={3}>
{values.rules.map((rule: ISecurityRule, index: number) => {
return (
<Paper
key={rule.security_rule_id}
variant="outlined"
sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
p: 1,
mb: 1
}}>
<SecurityRuleCard key={rule.security_rule_id} title={rule.name} subtitle={rule.description} />
<IconButton onClick={() => helpers.remove(index)}>
<Icon path={mdiClose} size={1} />
</IconButton>
</Paper>
);
})}
</Box>
</>
)}
</FieldArray>
</Box>
</form>
);
Expand Down
12 changes: 12 additions & 0 deletions app/src/utils/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DATE_FORMAT, TIME_FORMAT } from 'constants/dateTimeFormats';
import { IConfig } from 'contexts/configContext';
import { Feature, Polygon } from 'geojson';
import { LatLngBounds } from 'leaflet';
import _ from 'lodash';
import moment from 'moment';

/**
Expand Down Expand Up @@ -337,3 +338,14 @@ export const getTitle = (pageName?: string) => {
export const pluralize = (quantity: number, word: string, singularSuffix = '', pluralSuffix = 's') => {
return `${word}${quantity === 1 ? singularSuffix : pluralSuffix}`;
};

/**
* For a given property, alphabetize an array of objects
*
* @param {T[]} data an array of objects to be alphabetize
* @param {string} property a key property to alphabetize the data array on
* @returns {any[]} Returns an alphabetized array of objects
*/
export const alphabetizeObjects = <T extends { [key: string]: any }>(data: T[], property: string) => {
return _.sortBy(data, property);
};

0 comments on commit 6b9c797

Please sign in to comment.