Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Configuration/webapp/app/api/query/useConfigurationQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import { useQuery } from '@tanstack/react-query';
import axiosInstance from '../axiosInstance';
import type { FormItem } from '~/components/form/Form';
import type { FormValue } from '~/components/form/types';

export const CONFIGURATION_QUERY_KEY = 'configuration';

Expand All @@ -23,6 +23,6 @@ export const useConfigurationQuery = (configuration: string) =>
queryKey: [CONFIGURATION_QUERY_KEY, configuration],
queryFn: async () =>
axiosInstance
.get<FormItem>(`configurations/${configuration}`)
.get<FormValue>(`configurations/${configuration}`)
.then((response) => response.data),
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import { useQuery } from '@tanstack/react-query';
import axiosInstance from '../axiosInstance';
import type { FormRestrictions } from '~/components/form/Form';
import type { ObjectRestrictions } from '~/components/form/types';

export const CONFIGURATION_RESTRICTIONS_QUERY_KEY = 'configuration-restrictions';

Expand All @@ -23,6 +23,6 @@ export const useConfigurationRestrictionsQuery = (configuration: string) =>
queryKey: [CONFIGURATION_RESTRICTIONS_QUERY_KEY, configuration],
queryFn: async () =>
axiosInstance
.get<FormRestrictions>(`configurations/restrictions/${configuration}`)
.get<ObjectRestrictions>(`configurations/restrictions/${configuration}`)
.then((response) => response.data),
});
132 changes: 51 additions & 81 deletions Configuration/webapp/app/components/form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,102 +12,72 @@
* or submit itself to any jurisdiction.
*/

import { useCallback, useState, type FC, type PropsWithChildren } from 'react';
import Accordion from '@mui/material/Accordion';
import AccordionDetails from '@mui/material/AccordionDetails';
import Stack from '@mui/material/Stack';

import type { FC } from 'react';
import { Widget } from './components/Widget';
import { AccordionHeader } from './components/AccordionHeader';
import { RawViewModal } from './raw-view/RawViewModal';
import { ArrayWidget } from './components/widgets/ArrayWidget';
import type {
ArrayRestrictions,
FormArrayValue,
FormObjectValue,
FormPrimitiveValue,
FormValue,
ObjectRestrictions,
Restrictions,
} from './types';
import { isArrayRestrictions, isObjectRestrictions } from './types/helpers';
import { ObjectWidget } from './components/widgets/ObjectWidget';
import type { Control } from 'react-hook-form';
import { type InputsType } from '~/routes/configuration';
import { KEY_SEPARATOR } from './constants';

export type FormItem = { [key: string]: string | object | FormItem };

export type FormRestrictions = {
[key: string]: 'string' | 'number' | 'boolean' | 'array' | FormRestrictions;
};
import type { InputsType } from '~/routes/configuration';

interface FormProps extends PropsWithChildren {
interface FormProps {
sectionTitle: string;
sectionPrefix: string;
items: FormItem;
itemsRestrictions: FormRestrictions;
value: FormValue;
restrictions: Restrictions | ObjectRestrictions | ArrayRestrictions;
control: Control<InputsType>;
}

/**
* Function which returns false if the given object
* which describes restrictions is the leaf (string, number, bool, array)
* or returns true if the given object describes restrictions recursively
* @param {'string' | 'number' | 'boolean' | 'array' | FormRestrictions} obj
* the object which describes restrictions
* @returns {boolean} value which indicates if the restrictions are recursive
* or if this is the leaf of the FormRestrictions tree
*/
function isFormRestrictions(obj: FormRestrictions[string]): obj is FormRestrictions {
return obj instanceof Object && !(obj instanceof Array);
}

/**
* Form component.
* @param {FormProps} props - The props of the form.
* @param {string} props.sectionTitle - The title of the section.
* @param {string} props.sectionPrefix - The prefix of the section.
* @param {FormItem} props.items - The items of the form.
* @param {FormRestrictions} props.itemsRestrictions - The restrictions of the items.
* @param {Control<InputsType>} props.control - The control of the form.
* @returns {ReactElement} The form component.
*/
export const Form: FC<FormProps> = ({
sectionTitle,
sectionPrefix,
items,
itemsRestrictions,
value,
restrictions,
control,
}) => {
const [isRawModalOpen, setIsRawModalOpen] = useState<boolean>(false);
if (isObjectRestrictions(restrictions)) {
return (
<ObjectWidget
key={sectionTitle}
sectionTitle={sectionTitle}
sectionPrefix={sectionPrefix}
items={value as FormObjectValue}
itemsRestrictions={restrictions}
control={control}
/>
);
}

const renderItem = useCallback(
(key: string, value: FormRestrictions[string]) =>
isFormRestrictions(value) ? (
<Form
key={key}
sectionTitle={key}
sectionPrefix={`${sectionPrefix}${KEY_SEPARATOR}${key}`}
items={items[key] as FormItem}
itemsRestrictions={itemsRestrictions[key] as FormRestrictions}
control={control}
/>
) : (
<Widget
key={key}
sectionPrefix={`${sectionPrefix}${KEY_SEPARATOR}${key}`}
label={key}
type={value}
value={items[key]}
control={control}
/>
),
[items, itemsRestrictions],
);
if (isArrayRestrictions(restrictions)) {
return (
<ArrayWidget
key={sectionTitle}
sectionTitle={sectionTitle}
sectionPrefix={sectionPrefix}
items={value as Array<FormArrayValue>}
itemsRestrictions={restrictions}
control={control}
/>
);
}

return (
<>
<Accordion defaultExpanded>
<AccordionHeader title={sectionTitle} showRawViewModal={() => setIsRawModalOpen(true)} />
<AccordionDetails>
<Stack spacing={2}>
{Object.entries(itemsRestrictions).map(([key, value]) => renderItem(key, value))}
</Stack>
</AccordionDetails>
</Accordion>

{isRawModalOpen && (
<RawViewModal onClose={() => setIsRawModalOpen(false)} title={sectionTitle} data={items} />
)}
</>
<Widget
key={sectionTitle}
label={sectionTitle}
sectionPrefix={sectionPrefix}
type={restrictions}
value={value as FormPrimitiveValue}
control={control}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ import { useFormState, type Control } from 'react-hook-form';
import type { InputsType } from '~/routes/configuration';
import { FormTextInput } from './widgets/FormTextInput';
import { FormToggleInput } from './widgets/FormToggleInput';
import type { FormPrimitiveValue, PrimitiveRestrictions } from '../types';

export interface WidgetProps extends PropsWithChildren {
sectionPrefix: string;
label: string;
type: 'string' | 'number' | 'boolean' | 'array';
value: unknown;
type: PrimitiveRestrictions;
value: FormPrimitiveValue;
control: Control<InputsType>;
}

Expand All @@ -45,7 +46,7 @@ export const Widget: FC<WidgetProps> = ({ type, ...rest }): ReactElement => {
return <FormTextInput {...rest} isDirty={Boolean(isDirty)} type="number" />;
case 'boolean':
return <FormToggleInput {...rest} isDirty={Boolean(isDirty)} />;
case 'array':
return <>array not implemented</>; // TODO OGUI-1803: add implementation after the decision is made
default:
return <>unknown widget type: {JSON.stringify(type)}</>;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* @license
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/

import { useState, type ReactElement } from 'react';
import { Accordion, AccordionDetails, Stack } from '@mui/material';
import { AccordionHeader } from '../AccordionHeader';
import type { ArrayRestrictions, FormArrayValue } from '../../types';
import { RawViewModal } from '../../raw-view/RawViewModal';
import { Form } from '../../Form';
import type { Control } from 'react-hook-form';
import type { InputsType } from '~/routes/configuration';
import { KEY_SEPARATOR } from '../../constants';

interface ArrayWidgetProps {
sectionTitle: string;
sectionPrefix: string;
items: FormArrayValue;
itemsRestrictions: ArrayRestrictions;
control: Control<InputsType>;
}

export const ArrayWidget = ({
sectionTitle,
sectionPrefix,
items,
itemsRestrictions,
control,
}: ArrayWidgetProps): ReactElement => {
const [isRawModalOpen, setIsRawModalOpen] = useState(false);
const [arrayRestrictions] = itemsRestrictions; // [arrayRestrictions, objectBlueprint, arrayBlueprint]

return (
<>
<Accordion defaultExpanded>
<AccordionHeader title={sectionTitle} showRawViewModal={() => setIsRawModalOpen(true)} />
<AccordionDetails>
<Stack spacing={2}>
{items.map((item, idx) => (
<Form
key={idx}
sectionTitle={`Item #${idx}`}
sectionPrefix={`${sectionPrefix}${KEY_SEPARATOR}${idx}`}
value={item}
restrictions={arrayRestrictions[idx] ?? [[], null, null]}
control={control}
/>
))}
</Stack>
</AccordionDetails>
</Accordion>

{isRawModalOpen && (
<RawViewModal onClose={() => setIsRawModalOpen(false)} title={sectionTitle} data={items} />
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @license
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/

import { useState, type FC, type PropsWithChildren } from 'react';
import Accordion from '@mui/material/Accordion';
import AccordionDetails from '@mui/material/AccordionDetails';
import Stack from '@mui/material/Stack';
import { AccordionHeader } from '../AccordionHeader';
import { RawViewModal } from '../../raw-view/RawViewModal';
import type { Control } from 'react-hook-form';
import { type InputsType } from '~/routes/configuration';
import type { FormObjectValue, ObjectRestrictions } from '../../types';
import { KEY_SEPARATOR } from '../../constants';
import { Form } from '../../Form';

interface ObjectWidgetProps extends PropsWithChildren {
sectionTitle: string;
sectionPrefix: string;
items: FormObjectValue;
itemsRestrictions: ObjectRestrictions;
control: Control<InputsType>;
}

/**
* Form component.
* @param {ObjectWidgetProps} props - The props of the form.
* @param {string} props.sectionTitle - The title of the section.
* @param {string} props.sectionPrefix - The prefix of the section.
* @param {FormItem} props.items - The items of the form.
* @param {FormRestrictions} props.itemsRestrictions - The restrictions of the items.
* @param {Control<InputsType>} props.control - The control of the form.
* @returns {ReactElement} The form component.
*/
export const ObjectWidget: FC<ObjectWidgetProps> = ({
sectionTitle,
sectionPrefix,
items,
itemsRestrictions,
control,
}) => {
const [isRawModalOpen, setIsRawModalOpen] = useState<boolean>(false);

return (
<>
<Accordion defaultExpanded>
<AccordionHeader title={sectionTitle} showRawViewModal={() => setIsRawModalOpen(true)} />
<AccordionDetails>
<Stack spacing={2}>
{Object.entries(items).map(([key, value]) => (
<Form
key={key}
sectionTitle={key}
sectionPrefix={`${sectionPrefix}${KEY_SEPARATOR}${key}`}
value={value}
restrictions={itemsRestrictions[key]}
control={control}
/>
))}
</Stack>
</AccordionDetails>
</Accordion>

{isRawModalOpen && (
<RawViewModal onClose={() => setIsRawModalOpen(false)} title={sectionTitle} data={items} />
)}
</>
);
};
Loading