Skip to content

Commit

Permalink
Implement the Configuration page
Browse files Browse the repository at this point in the history
Fixes: freeipa#567

Signed-off-by: Mark Reynolds <[email protected]>
  • Loading branch information
mreynolds389 committed Sep 27, 2024
1 parent 09e940d commit 25d9cf4
Show file tree
Hide file tree
Showing 20 changed files with 1,574 additions and 11 deletions.
102 changes: 102 additions & 0 deletions src/components/Form/IpaDropdownSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React from "react";
// PatternFly
import {
Divider,
MenuToggle,
MenuToggleElement,
MenuSearch,
MenuSearchInput,
Dropdown,
DropdownItem,
DropdownList,
SearchInput,
} from "@patternfly/react-core";
// Utils
import {
IPAParamDefinition,
getParamProperties,
} from "src/utils/ipaObjectUtils";
import { updateIpaObject } from "src/utils/ipaObjectUtils";

interface IPAParamDefinitionDropdown extends IPAParamDefinition {
id?: string;
setIpaObject?: (ipaObject: Record<string, unknown>) => void;
options: string[];
ariaLabelledBy?: string;
onSearch: (value: string) => void;
}

const IpaDropdownSearch = (props: IPAParamDefinitionDropdown) => {
const [isOpen, setIsOpen] = React.useState(false);
const [searchValue, setSearchValue] = React.useState("");

const { value } = getParamProperties(props);
const ipaObject = props.ipaObject || {};

const onSelect = (
_event: React.MouseEvent<Element, MouseEvent> | undefined,
selection: string | number | undefined
) => {
if (ipaObject && props.setIpaObject !== undefined) {
updateIpaObject(ipaObject, props.setIpaObject, selection, props.name);
}
props.onSearch("");
setSearchValue("");
setIsOpen(false);
};

const onToggleClick = () => {
setIsOpen(!isOpen);
};

// Removed selected value from options
const options = props.options.filter((item) => item !== value);

return (
<Dropdown
id={props.id || "dropdown-search"}
isOpen={isOpen}
onSelect={onSelect}
onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)}
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleRef}
isFullWidth
onClick={onToggleClick}
isExpanded={isOpen}
>
{value}
</MenuToggle>
)}
ouiaId="BasicDropdown"
shouldFocusToggleOnSelect
isScrollable
>
<MenuSearch>
<MenuSearchInput>
<SearchInput
value={searchValue}
placeholder="Search"
onChange={(_event, value) => setSearchValue(value)}
onSearch={() => props.onSearch(searchValue)}
onClear={() => {
setSearchValue("");
props.onSearch("");
}}
aria-labelledby="pf-v5-context-selector-search-button-id-1"
/>
</MenuSearchInput>
</MenuSearch>
<Divider />
<DropdownList>
{options.map((option, index) => (
<DropdownItem value={option} key={index}>
{option}
</DropdownItem>
))}
</DropdownList>
</Dropdown>
);
};

export default IpaDropdownSearch;
16 changes: 6 additions & 10 deletions src/components/Form/IpaNumberInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
*/

interface IPAParamDefinitionNumberInput extends IPAParamDefinition {
id?: string;
className?: string;
numCharsShown?: number;
minValue?: number;
Expand All @@ -49,11 +50,8 @@ const IpaNumberInput = (props: IPAParamDefinitionNumberInput) => {
};

const onMinus = () => {
const newValue = normalizeBetween(
(value as number) - 1,
props.minValue,
props.maxValue
);
const num = value ? parseInt(value as string, 10) : 0;
const newValue = normalizeBetween(num - 1, props.minValue, props.maxValue);
onChange(newValue);
};

Expand Down Expand Up @@ -88,17 +86,15 @@ const IpaNumberInput = (props: IPAParamDefinitionNumberInput) => {
};

const onPlus = () => {
const newValue = normalizeBetween(
(value as number) + 1,
props.minValue,
props.maxValue
);
const num = value ? parseInt(value as string, 10) : 0;
const newValue = normalizeBetween(num + 1, props.minValue, props.maxValue);
onChange(newValue);
};

return (
<>
<NumberInput
id={props.id}
value={numberValue}
onMinus={onMinus}
onChange={onChangeHandler}
Expand Down
2 changes: 2 additions & 0 deletions src/components/layouts/PopoverWithIconLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface PropsToPopover {
hasNoPadding?: boolean;
withFocusTrap?: boolean;
hasAutoWidth?: boolean;
triggerHover?: boolean;
}

const PopoverWithIconLayout = (props: PropsToPopover) => {
Expand All @@ -19,6 +20,7 @@ const PopoverWithIconLayout = (props: PropsToPopover) => {
aria-label={
props.ariaLabel || "Popover with no header, footer, and close button"
}
triggerAction={props.triggerHover ? "hover" : "click"}
hasNoPadding={props.hasNoPadding || false}
showClose={props.showClose || false}
bodyContent={props.message}
Expand Down
114 changes: 114 additions & 0 deletions src/hooks/useConfigSettingsData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { useState, useEffect } from "react";

// RPC
import { useGetObjectMetadataQuery } from "src/services/rpc";
import { useGetConfigQuery } from "src/services/rpcConfig";
// Data types
import { Config, Metadata } from "src/utils/datatypes/globalDataTypes";

type ConfigSettingsData = {
isLoading: boolean;
isFetching: boolean;
modified: boolean;
setModified: (value: boolean) => void;
resetValues: () => void;
metadata: Metadata;
originalConfig: Partial<Config>;
config: Partial<Config>;
setConfig: (hostGroup: Partial<Config>) => void;
refetch: () => void;
modifiedValues: () => Partial<Config>;
};

const useConfigSettings = (): ConfigSettingsData => {
// [API call] Metadata
const metadataQuery = useGetObjectMetadataQuery();
const metadata = metadataQuery.data || {};
const metadataLoading = metadataQuery.isLoading;

// [API call] Config
const configQuery = useGetConfigQuery();
const configData = configQuery.data;
const isFullDataLoading = configQuery.isLoading;
const [modified, setModified] = useState(false);
const [config, setConfig] = useState<Partial<Config>>({});

useEffect(() => {
if (configData && !configQuery.isFetching) {
setConfig({ ...configData });
}
}, [configData, configQuery.isFetching]);

const settings = {
isLoading: metadataLoading || isFullDataLoading,
isFetching: configQuery.isFetching,
modified,
setModified,
metadata,
// eslint-disable-next-line @typescript-eslint/no-empty-function
resetValues: () => {},
originalConfig: config,
config,
setConfig,
refetch: configQuery.refetch,
modifiedValues: () => config,
} as ConfigSettingsData;

if (configData) {
settings.originalConfig = configData || {};
} else {
settings.originalConfig = {};
}

const getModifiedValues = (): Partial<Config> => {
if (!configData) {
return {};
}

const modifiedValues = {};
for (const [key, value] of Object.entries(config)) {
if (Array.isArray(value)) {
// Using 'JSON.stringify' when comparing arrays (to prevent data type false positives)
if (JSON.stringify(configData[key]) !== JSON.stringify(value)) {
modifiedValues[key] = value;
}
} else if (configData[key] !== value) {
modifiedValues[key] = value;
}
}
return modifiedValues;
};
settings.modifiedValues = getModifiedValues;

// Detect any change in 'originalConfig' and 'config' objects
useEffect(() => {
if (!configData) {
return;
}
let modified = false;
for (const [key, value] of Object.entries(config)) {
if (Array.isArray(value)) {
// Using 'JSON.stringify' when comparing arrays (to prevent data type false positives)
if (JSON.stringify(configData[key]) !== JSON.stringify(value)) {
modified = true;
break;
}
} else {
if (configData[key].toString() !== value.toString()) {
modified = true;
break;
}
}
}
setModified(modified);
}, [config, configData]);

const onResetValues = () => {
setModified(false);
};
settings.resetValues = onResetValues;

return settings;
};

export { useConfigSettings };
2 changes: 1 addition & 1 deletion src/hooks/useSudoRuleSettingsData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const useSudoRuleSettings = (ruleId: string): SettingsData => {
break;
}
} else {
if (ruleFullData.rule[key] !== value) {
if (ruleFullData.rule[key].toString() !== value.toString()) {
modified = true;
break;
}
Expand Down
2 changes: 2 additions & 0 deletions src/navigation/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import HBACRulesTabs from "src/pages/HBACRules/HBACRulesTabs";
import HBACServiceGroupsTabs from "src/pages/HBACServiceGroups/HBACServiceGroupsTabs";
import ResetPasswordPage from "src/login/ResetPasswordPage";
import SetupBrowserConfig from "src/pages/SetupBrowserConfig";
import Configuration from "src/pages/Configuration/Configuration";

// Renders routes (React)
export const AppRoutes = ({ isInitialDataLoaded }): React.ReactElement => {
Expand Down Expand Up @@ -372,6 +373,7 @@ export const AppRoutes = ({ isInitialDataLoaded }): React.ReactElement => {
<Route path="kerberos-ticket-policy">
<Route path="" element={<KrbTicketPolicy />} />
</Route>
<Route path="configuration" element={<Configuration />} />
{/* Redirect to Active users page if user is logged in and navigates to the root page */}
<Route
path="*"
Expand Down
18 changes: 18 additions & 0 deletions src/navigation/NavRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ const SelinuxUserMapsGroupRef = "selinux-user-maps";
const PasswordPoliciesGroupRef = "password-policies";
// - Kerberos ticket policy
const KerberosTicketPolicyGroupRef = "kerberos-ticket-policy";
// IPA SERVER
// - Configuration
const ConfigRef = "configuration";

// List of navigation routes (UI)
export const navigationRoutes = [
Expand Down Expand Up @@ -259,6 +262,21 @@ export const navigationRoutes = [
},
],
},
{
label: "IPA Server",
group: "",
title: `${BASE_TITLE} - IPA Server`,
path: "",
items: [
{
label: "Configuration",
group: ConfigRef,
title: `${BASE_TITLE} - Configuration`,
path: "configuration",
items: [],
},
],
},
];

/**
Expand Down
57 changes: 57 additions & 0 deletions src/pages/Configuration/ConfigGroupOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from "react";
// PatternFly
import { Flex, FlexItem, Form, FormGroup } from "@patternfly/react-core";
// Data types
import { Metadata } from "src/utils/datatypes/globalDataTypes";
// Form
import IpaTextArea from "src/components/Form/IpaTextArea";
import ConfigObjectclassTable from "./ConfigObjectclassTable";

interface PropsToGroupOptions {
// 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 ConfigGroupOptions = (props: PropsToGroupOptions) => {
return (
<Flex direction={{ default: "column" }} flex={{ default: "flex_1" }}>
<FlexItem flex={{ default: "flex_1" }}>
<Form className="pf-v5-u-mb-lg pf-v5-u-mt-lg" isHorizontal>
<FormGroup
label="Group search fields"
fieldId="ipagroupsearchfields"
isRequired
>
<IpaTextArea
name="ipagroupsearchfields"
ipaObject={props.ipaObject}
onChange={props.recordOnChange}
objectName="config"
metadata={props.metadata}
/>
</FormGroup>
</Form>
<Form className="pf-v5-u-mb-lg pf-v5-u-mt-lg">
<FormGroup
label="Default group objectclasses"
fieldId="ipagroupobjectclasses"
>
<ConfigObjectclassTable
title="Default group objectclasses"
name="ipagroupobjectclasses"
ipaObject={props.ipaObject}
onChange={props.recordOnChange}
objectName="config"
metadata={props.metadata}
/>
</FormGroup>
</Form>
</FlexItem>
</Flex>
);
};

export default ConfigGroupOptions;
Loading

0 comments on commit 25d9cf4

Please sign in to comment.