Skip to content

Commit

Permalink
Add 'General' section
Browse files Browse the repository at this point in the history
The 'General' section from 'Sudo rules' > 'Settings'
should show information about the rule name, sudo
order, and description (if any). At the same time,
it should include the action buttons functionality
('Refresh', 'Revert', and 'Save').

Signed-off-by: Carla Martinez <[email protected]>
  • Loading branch information
carma12 committed Sep 20, 2024
1 parent b7db382 commit cd2ac65
Show file tree
Hide file tree
Showing 8 changed files with 519 additions and 5 deletions.
126 changes: 126 additions & 0 deletions src/components/Form/IpaNumberInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import React from "react";
// PatternFly
import {
HelperText,
HelperTextItem,
NumberInput,
} from "@patternfly/react-core";
// IPA Object
import {
getParamProperties,
IPAParamDefinition,
} from "src/utils/ipaObjectUtils";

/**
* Interface for the IPA Number Input
* @param {string} className - The class name for the input (Optional. Default: "")
* @param {number} numCharsShown - The number of characters shown in the input (Optional. Default: 1)
* @param {number} minValue - The minimum value allowed (Optional)
* @param {number} maxValue - The maximum value allowed (Optional)
* @param {IPAParamDefinition} IPAParamDefinition - IPA Object parameters
* @returns {React.ReactNode} The IPA Number Input component
*
*/

interface IPAParamDefinitionNumberInput extends IPAParamDefinition {
className?: string;
numCharsShown?: number;
minValue?: number;
maxValue?: number;
}

const IpaNumberInput = (props: IPAParamDefinitionNumberInput) => {
const { readOnly, value, onChange } = getParamProperties(props);

const numberValue = value as number;

const [isError, setIsError] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState("");

const normalizeBetween = (value, min, max) => {
if (min !== undefined && max !== undefined) {
return Math.max(Math.min(value, max), min);
} else if (value <= min) {
return min;
} else if (value >= max) {
return max;
}
return value;
};

const onMinus = () => {
const newValue = normalizeBetween(
(value as number) - 1,
props.minValue,
props.maxValue
);
onChange(newValue);
};

const onChangeHandler = (event: React.FormEvent<HTMLInputElement>) => {
const value = (event.target as HTMLInputElement).value;
onChange(value === "" ? value : +value);

// Return to max and min values if the input is beyond those values
if (props.minValue && +value < props.minValue) {
onChange(props.minValue);
} else if (props.maxValue && +value > props.maxValue) {
onChange(props.maxValue);
}

// Show error message if other characters are entered
if (isNaN(+value) || /[a-zA-Z]/.test(value)) {
onChange(value as string);
setIsError(true);
setErrorMessage("Only numbers are allowed.");
return;
}
};

const onBlur = (event: React.FocusEvent<HTMLInputElement>) => {
const blurVal = +event.target.value;

if (props.minValue && blurVal < props.minValue) {
onChange(props.minValue);
} else if (props.maxValue && blurVal > props.maxValue) {
onChange(props.maxValue);
}
};

const onPlus = () => {
const newValue = normalizeBetween(
(value as number) + 1,
props.minValue,
props.maxValue
);
onChange(newValue);
};

return (
<>
<NumberInput
value={numberValue}
onMinus={onMinus}
onChange={onChangeHandler}
min={props.minValue}
max={props.maxValue}
onBlur={onBlur}
onPlus={onPlus}
inputName="input"
inputAriaLabel="number input"
minusBtnAriaLabel="minus"
plusBtnAriaLabel="plus"
isDisabled={readOnly}
widthChars={props.numCharsShown || 1}
className={props.className || ""}
/>
{isError && (
<HelperText>
<HelperTextItem>{errorMessage}</HelperTextItem>
</HelperText>
)}
</>
);
};

export default IpaNumberInput;
58 changes: 58 additions & 0 deletions src/components/SudoRuleSections/SudoRuleGeneral.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from "react";
// PatternFly
import { Form, FormGroup } from "@patternfly/react-core";
// Ipa Components
import IpaTextInput from "../Form/IpaTextInput";
import IpaTextArea from "../Form/IpaTextArea";
import IpaNumberInput from "../Form/IpaNumberInput";
// Data types
import { Metadata } from "src/utils/datatypes/globalDataTypes";

interface PropsToSudoRuleGeneral {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ipaObject: Record<string, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
recordOnChange: (ipaObject: Record<string, any>) => void;
metadata: Metadata;
}

const SudoRuleGeneral = (props: PropsToSudoRuleGeneral) => {
return (
<Form className="pf-v5-u-mt-sm pf-v5-u-mb-lg pf-v5-u-mr-md" isHorizontal>
<FormGroup label="Rule name" fieldId="rule-name">
<IpaTextInput
name="cn"
aria-label="rule name"
ipaObject={props.ipaObject}
onChange={props.recordOnChange}
objectName="sudorule"
metadata={props.metadata}
/>
</FormGroup>
<FormGroup label="Sudo order" fieldId="sudo-order">
<IpaNumberInput
name="sudoorder"
aria-label="sudo order"
ipaObject={props.ipaObject}
onChange={props.recordOnChange}
objectName="sudorule"
metadata={props.metadata}
numCharsShown={15}
minValue={0}
maxValue={2147483647}
/>
</FormGroup>
<FormGroup label="Description" fieldId="description">
<IpaTextArea
name="description"
ipaObject={props.ipaObject}
onChange={props.recordOnChange}
objectName="sudorule"
metadata={props.metadata}
/>
</FormGroup>
</Form>
);
};

export default SudoRuleGeneral;
63 changes: 63 additions & 0 deletions src/components/layouts/SidebarLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from "react";
// PatternFly
import {
JumpLinks,
JumpLinksItem,
Sidebar,
SidebarContent,
SidebarPanel,
TextVariants,
} from "@patternfly/react-core";
import HelpTextWithIconLayout from "./HelpTextWithIconLayout";
// Icons
import OutlinedQuestionCircleIcon from "@patternfly/react-icons/dist/esm/icons/outlined-question-circle-icon";

interface SidebarLayoutProps {
itemNames: string[];
children: React.ReactNode;
}

const SidebarLayout = (props: SidebarLayoutProps) => {
// Utility functions
const parseNameToId = (name: string) => {
return name.toLowerCase().replace(/ /g, "-");
};

// Render component
return (
<>
<Sidebar isPanelRight>
<SidebarPanel variant="sticky">
<HelpTextWithIconLayout
textComponent={TextVariants.p}
textClassName="pf-v5-u-mb-md"
subTextComponent={TextVariants.a}
subTextIsVisitedLink={true}
textContent="Help"
icon={
<OutlinedQuestionCircleIcon className="pf-v5-u-primary-color-100 pf-v5-u-mr-sm" />
}
/>
<JumpLinks
isVertical
label="Jump to section"
scrollableSelector="#settings-page"
offset={220} // for masthead
expandable={{ default: "expandable", md: "nonExpandable" }}
>
{props.itemNames.map((item, index) => (
<JumpLinksItem key={index} href={"#" + parseNameToId(item)}>
{item}
</JumpLinksItem>
))}
</JumpLinks>
</SidebarPanel>
<SidebarContent className="pf-v5-u-mr-xl">
{props.children}
</SidebarContent>
</Sidebar>
</>
);
};

export default SidebarLayout;
5 changes: 5 additions & 0 deletions src/navigation/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import HBACServices from "src/pages/HBACServices/HBACServices";
import HBACServiceGroups from "src/pages/HBACServiceGroups/HBACServiceGroups";
import HBACTest from "src/pages/HBACTest/HBACTest";
import SudoRules from "src/pages/SudoRules/SudoRules";
import SudoRulesTabs from "src/pages/SudoRules/SudoRulesTabs";
import SudoCmds from "src/pages/SudoCmds/SudoCmds";
import SudoCmdsTabs from "src/pages/SudoCmds/SudoCmdsTabs";
import SudoCmdGroups from "src/pages/SudoCmdGroups/SudoCmdGroups";
Expand Down Expand Up @@ -331,6 +332,10 @@ export const AppRoutes = ({ isInitialDataLoaded }): React.ReactElement => {
</Route>
<Route path="sudo-rules">
<Route path="" element={<SudoRules />} />
<Route
path=":cn"
element={<SudoRulesTabs section="settings" />}
/>
</Route>
<Route path="sudo-commands">
<Route path="" element={<SudoCmds />} />
Expand Down
Loading

0 comments on commit cd2ac65

Please sign in to comment.