Skip to content

Commit

Permalink
Merge pull request PelicanPlatform#1843 from CannonLock/issue-1723-en…
Browse files Browse the repository at this point in the history
…hance

Add PolicyDefinitions UI
  • Loading branch information
jhiemstrawisc authored Dec 18, 2024
2 parents 70c9986 + 0d1e02f commit 21c426d
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 25 deletions.
2 changes: 1 addition & 1 deletion .github/scripts/validate-parameters/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"AuthorizationTemplates",
"IPMapping",
"Exports",
"Lots"
"PolicyDefinitions",
]


Expand Down
12 changes: 7 additions & 5 deletions web_ui/frontend/components/configuration/Fields/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
LotForm,
IPMappingForm,
AuthorizationTemplateForm,
PolicyDefinitionForm,
PolicyDefinition,
} from '@/components/configuration';
import { buildPatch } from '@/components/configuration/util';
import { LoadingField } from '@/components/configuration/Fields/LoadingField';
Expand Down Expand Up @@ -190,15 +192,15 @@ const Field = ({
keyGetter={(v) => v.federationprefix}
/>
);
case 'Lots':
case 'PolicyDefinitions':
return (
<ObjectField
focused={focused}
onChange={handleChange<Lot[]>}
onChange={handleChange<PolicyDefinition[]>}
name={name}
value={value as Lot[]}
Form={LotForm}
keyGetter={(v) => v.lotname}
value={value as PolicyDefinition[]}
Form={PolicyDefinitionForm}
keyGetter={(v) => v.policyname}
/>
);
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React, { useCallback } from 'react';
import { Box, Button } from '@mui/material';

import {
FormProps,
PolicyDefinition,
Lot,
StringSliceField,
} from '@/components/configuration';
import {
StringField,
BooleanField,
ObjectField,
LotForm,
} from '@/components/configuration';

const verifyForm = (x: PolicyDefinition) => {
return x.policyname != '' && x.purgeorder.length == 4;
};

const createDefaultPolicyDefinition = (): PolicyDefinition => {
return {
policyname: '',
purgeorder: ['del', 'exp', 'opp', 'ded'],
discoverprefixes: false,
mergelocalwithdiscovered: false,
divideunallocated: false,
lots: [],
};
};

const PolicyDefinitionForm = ({
onSubmit,
value,
}: FormProps<PolicyDefinition>) => {
const [policyDefinition, setPolicyDefinition] =
React.useState<PolicyDefinition>(value || createDefaultPolicyDefinition());

const submitHandler = useCallback(() => {
if (!verifyForm(policyDefinition)) {
return;
}
onSubmit(policyDefinition);
}, [policyDefinition]);

return (
<>
<Box my={2}>
<StringField
name={'PolicyName'}
onChange={(e) =>
setPolicyDefinition({ ...policyDefinition, policyname: e })
}
value={policyDefinition.policyname}
/>
</Box>
<Box mb={2}>
<StringSliceField
name={'PurgeOrder'}
onChange={(e) =>
setPolicyDefinition({
...policyDefinition,
purgeorder: e as PolicyDefinition['purgeorder'],
})
}
value={policyDefinition.purgeorder}
verify={verifyPurgeOrder}
/>
</Box>
<Box mb={2}>
<BooleanField
name={'DiscoverPrefixes'}
onChange={(e) =>
setPolicyDefinition({ ...policyDefinition, discoverprefixes: e })
}
value={policyDefinition.discoverprefixes}
/>
</Box>
<Box mb={2}>
<BooleanField
name={'MergeLocalWithDiscovered'}
onChange={(e) =>
setPolicyDefinition({
...policyDefinition,
mergelocalwithdiscovered: e,
})
}
value={policyDefinition.mergelocalwithdiscovered}
/>
</Box>
<Box mb={2}>
<BooleanField
name={'DivideUnallocated'}
onChange={(e) =>
setPolicyDefinition({ ...policyDefinition, divideunallocated: e })
}
value={policyDefinition.divideunallocated}
/>
</Box>
<Box mb={2}>
<ObjectField
name={'Lots'}
onChange={(e) =>
setPolicyDefinition({ ...policyDefinition, lots: e })
}
value={policyDefinition.lots}
Form={LotForm}
keyGetter={(x: Lot) => x.lotname}
/>
</Box>
<Button type={'submit'} onClick={submitHandler}>
Submit
</Button>
</>
);
};

const verifyPurgeOrder = (x: string[]) => {
// Check the required values are present
if (
!(
x.includes('del') &&
x.includes('exp') &&
x.includes('opp') &&
x.includes('ded')
)
) {
return "Purge order must contain 'del', 'exp', 'opp', and 'ded'";
}

// Check that only valid values are present
if (x.length != 4) {
return 'Purge order must contain exactly [del, exp, opp, ded] in user defined order';
}
};

export default PolicyDefinitionForm;
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ExportForm from './ExportForm';
import LotForm from './LotForm';
import PathForm from './PathForm';
import AuthorizationTemplateForm from './AuthorizationTemplateForm';
import PolicyDefinitionForm from './PolicyDefinitionForm';

export {
AuthorizationTemplateForm,
Expand All @@ -21,4 +22,5 @@ export {
ExportForm,
LotForm,
PathForm,
PolicyDefinitionForm,
};
108 changes: 90 additions & 18 deletions web_ui/frontend/components/configuration/Fields/StringSliceField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ import {
Edit,
Close,
} from '@mui/icons-material';
import React, { useMemo, useCallback, ChangeEvent, KeyboardEvent } from 'react';
import React, {
useMemo,
useCallback,
ChangeEvent,
KeyboardEvent,
useEffect,
} from 'react';

import { createId, buildPatch, stringSort } from '../util';

Expand All @@ -26,17 +32,23 @@ export type StringSliceFieldProps = {
value: string[];
focused?: boolean;
onChange: (value: string[]) => void;
verify?: (value: string[]) => string | undefined;
};

const StringSliceField = ({
onChange,
name,
value,
focused,
verify,
}: StringSliceFieldProps) => {
const id = useMemo(() => createId(name), [name]);

const transformedValue = useMemo(() => (value == null ? [] : value), [value]);
// Hold a buffer value so that you can type freely without saving an invalid state
const [bufferValue, setBufferValue] = React.useState(value || []);
useEffect(() => {
setBufferValue(value || []);
}, [value]);

const [inputValue, setInputValue] = React.useState<string>('');

Expand All @@ -52,16 +64,35 @@ const StringSliceField = ({
event.target.value != ''
) {
const newValue = [
...new Set<string>([...transformedValue, event.target.value]),
].sort(stringSort);
...new Set<string>([event.target.value, ...bufferValue]),
];

onChange(newValue);
setBufferValue(newValue);
setInputValue('');

if (verify && verify(newValue) !== undefined) {
return;
}

onChange(newValue);
}
},
[onChange, inputValue]
);

const error = useMemo(
() => (verify ? verify(bufferValue) : undefined),
[bufferValue]
);

const helperText = useMemo(() => {
if (error) {
return error;
} else if (inputValue != '') {
return 'Press enter to add';
}
}, [error, inputValue]);

return (
<>
<TextField
Expand All @@ -72,12 +103,13 @@ const StringSliceField = ({
variant={'outlined'}
value={inputValue}
focused={focused}
helperText={inputValue == '' ? undefined : 'Press enter to add'}
helperText={helperText}
onChange={(e) => setInputValue(e.target.value)}
InputProps={{ onKeyDown: handleKeyDown }}
error={error !== undefined}
/>
<Box>
{transformedValue.length > 0 && (
{bufferValue.length > 0 && (
<Box
sx={{
display: 'flex',
Expand Down Expand Up @@ -112,7 +144,7 @@ const StringSliceField = ({
</Tooltip>
<Box ml={1} display={'inline'}>
<Typography variant={'caption'}>
{transformedValue.length} Items
{bufferValue.length} Items
</Typography>
</Box>
</Box>
Expand All @@ -123,21 +155,43 @@ const StringSliceField = ({
maxHeight: dropdownHeight,
overflowY: 'scroll',
borderBottom:
transformedValue.length == 0 || dropdownHeight == '0px'
bufferValue.length == 0 || dropdownHeight == '0px'
? undefined
: 'black 1px solid',
}}
>
{transformedValue.map((val) => {
{bufferValue.map((val) => {
return (
<StringSliceCard
key={val}
value={val}
onClick={() => {
const newValue = transformedValue.filter((v) => v != val);
onDelete={() => {
const newValue = bufferValue.filter((v) => v != val);
onChange(newValue);
setInputValue(val);
}}
onMoveUp={() => {
const index = bufferValue.indexOf(val);
if (index > 0) {
const newValue = [...bufferValue];
[newValue[index - 1], newValue[index]] = [
newValue[index],
newValue[index - 1],
];
onChange(newValue);
}
}}
onMoveDown={() => {
const index = bufferValue.indexOf(val);
if (index < bufferValue.length - 1) {
const newValue = [...bufferValue];
[newValue[index + 1], newValue[index]] = [
newValue[index],
newValue[index + 1],
];
onChange(newValue);
}
}}
/>
);
})}
Expand All @@ -149,10 +203,17 @@ const StringSliceField = ({

interface StringSliceCardProps {
value: string;
onClick: () => void;
onDelete: () => void;
onMoveUp: () => void;
onMoveDown: () => void;
}

const StringSliceCard = ({ value, onClick }: StringSliceCardProps) => {
const StringSliceCard = ({
value,
onDelete,
onMoveDown,
onMoveUp,
}: StringSliceCardProps) => {
return (
<Box
sx={{
Expand All @@ -169,7 +230,6 @@ const StringSliceCard = ({ value, onClick }: StringSliceCardProps) => {
borderColor: 'primary.light',
},
}}
onClick={onClick}
>
<Box
sx={{
Expand All @@ -179,9 +239,21 @@ const StringSliceCard = ({ value, onClick }: StringSliceCardProps) => {
>
{value}
</Box>
<IconButton size={'small'}>
<Close />
</IconButton>
<Box>
<Tooltip title={'Move Up'} onClick={onMoveUp}>
<IconButton size={'small'}>
<KeyboardArrowUp />
</IconButton>
</Tooltip>
<Tooltip title={'Move Down'}>
<IconButton size={'small'} onClick={onMoveDown}>
<KeyboardArrowDown />
</IconButton>
</Tooltip>
<IconButton size={'small'} onClick={onDelete}>
<Close />
</IconButton>
</Box>
</Box>
);
};
Expand Down
Loading

0 comments on commit 21c426d

Please sign in to comment.