Skip to content

Commit

Permalink
Added a loading state for FeaturesAttributesRows
Browse files Browse the repository at this point in the history
  • Loading branch information
lancegliser committed Dec 2, 2024
1 parent cfbd878 commit 2191412
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const meta: Meta<typeof FeaturesAttributesRows> = {
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
args: {
loading: false,
purposes: ["core"],
},
};
Expand Down Expand Up @@ -186,6 +187,23 @@ Synthesis.args!.timeFeatureAtom =
runRequiredAtom: synthesisRunRequiredAtom,
});

const loadingRunRequiredAtom = getInferFeatureAttributesRunRequiredFields();
export const Loading: Story = {
args: {
loading: true,
activeFeatureAtom: getFeatureAttributesActiveFeatureAtom(),
optionsAtom: getFeatureAttributesOptionsAtom({}),
paramsAtom: getInferFeatureAttributesParamsAtom({
features: sampleFeatureAttributesIndex,
}),
runRequiredAtom: loadingRunRequiredAtom,
},
};
Default.args!.timeFeatureAtom = getInferFeatureAttributesParamsTimeFeatureAtom({
paramsAtom: Default.args!.paramsAtom!,
runRequiredAtom: loadingRunRequiredAtom,
});

const noFeaturesRunRequiredAtom = getInferFeatureAttributesRunRequiredFields();
export const NoFeatures: Story = {
args: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ describe("FeaturesAttributesRows", () => {
);

const table = screen.getByRole("table");
const featureRows = within(table).getAllByRole("row").slice(1);
const featureRows = within(table)
.getAllByRole("row")
// Remove the header row
.slice(1);
expect(featureRows.length).toBe(features.length);
features.forEach((feature) => {
within(table).getByText(feature);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { FeatureAttributes } from "@howso/engine";
import {
ErrorBoundary,
FieldSelectProps,
FormModal,
Radio,
SecondaryButton,
Skeleton,
ToggleInput,
UpdateIcon,
WarningIcon,
Expand Down Expand Up @@ -55,9 +57,10 @@ import { FeaturesAttributesRowsI18nBundle as i18n } from "./FeaturesAttributesRo

export type FeaturesAttributesRowsProps = IFeatureAttributePurposes & {
activeFeatureAtom: FeatureAttributesActiveFeatureAtom;
runRequiredAtom: InferFeatureAttributesRunRequiredFieldsAtom;
paramsAtom: InferFeatureAttributesParamsAtom;
loading?: boolean;
optionsAtom: FeatureAttributesOptionsAtom;
paramsAtom: InferFeatureAttributesParamsAtom;
runRequiredAtom: InferFeatureAttributesRunRequiredFieldsAtom;
timeFeatureAtom: InferFeatureAttributesParamsTimeFeatureAtom;
};
/**
Expand All @@ -74,54 +77,37 @@ export const FeaturesAttributesRows: FC<FeaturesAttributesRowsProps> = ({
const { activeFeatureAtom, paramsAtom, optionsAtom, timeFeatureAtom } = props;
const activeFeature = useAtomValue(activeFeatureAtom);
const params = useAtomValue(paramsAtom);
const [options, setOptions] = useAtom(optionsAtom);
const setTimeFeature = useSetAtom(timeFeatureAtom);

const features = Object.keys(params.features || {});

// Toggle time series
const onChangeTimeSeries = (evt: ChangeEvent<HTMLInputElement>) => {
setOptions({ ...options, time_series: evt.currentTarget.checked });
if (!evt.currentTarget.checked) setTimeFeature(null);
};

return (
<FeaturesAttributesContextProvider purposes={purposes}>
<div className="relative overflow-x-auto rounded-lg shadow-md">
<Table striped>
<Table.Head>
<Table.HeadCell className="whitespace-nowrap">
{t(i18n.strings.headings.feature)}
</Table.HeadCell>
<Table.HeadCell className="whitespace-nowrap">
{t(i18n.strings.headings.sample)}
</Table.HeadCell>
<Table.HeadCell className="w-48 min-w-48 whitespace-nowrap">
{t(featureAttributeTypeLabel)}
</Table.HeadCell>
<Table.HeadCell className="w-[1%] whitespace-nowrap text-center">
<div className="flex items-center gap-2">
{options.time_series
? t(i18n.strings.headings.timeFeature)
: t(i18n.strings.headings.timeSeries)}
<ToggleInput
onChange={onChangeTimeSeries}
checked={options.time_series || false}
/>
</div>
</Table.HeadCell>
<Table.HeadCell className="w-[1%] whitespace-nowrap text-center">
{t(i18n.strings.headings.configuration)}
</Table.HeadCell>
<TableHeadCells
optionsAtom={optionsAtom}
timeFeatureAtom={timeFeatureAtom}
/>
</Table.Head>
<Table.Body className="divide-y">
{features.map((featureName) => (
<FeatureFields
key={featureName}
feature={featureName}
{...props}
/>
))}
{props.loading
? new Array(10)
.fill(0)
.map((_, index) => (
<FeatureFields
key={`loading:${index}`}
feature={undefined}
{...props}
/>
))
: features.map((featureName) => (
<FeatureFields
key={featureName}
feature={featureName}
{...props}
/>
))}
</Table.Body>
</Table>
</div>
Expand All @@ -136,22 +122,71 @@ export const FeaturesAttributesRows: FC<FeaturesAttributesRowsProps> = ({
);
};

type TableHeadCellsProps = {
optionsAtom: FeatureAttributesOptionsAtom;
timeFeatureAtom: InferFeatureAttributesParamsTimeFeatureAtom;
};
const TableHeadCells: FC<TableHeadCellsProps> = ({
optionsAtom,
timeFeatureAtom,
}) => {
const { t } = useTranslation(i18n.namespace);
const [options, setOptions] = useAtom(optionsAtom);
const setTimeFeature = useSetAtom(timeFeatureAtom);

// Toggle time series
const onChangeTimeSeries = (evt: ChangeEvent<HTMLInputElement>) => {
setOptions({ ...options, time_series: evt.currentTarget.checked });
if (!evt.currentTarget.checked) setTimeFeature(null);
};

return (
<>
<Table.HeadCell className="whitespace-nowrap">
{t(i18n.strings.headings.feature)}
</Table.HeadCell>
<Table.HeadCell className="whitespace-nowrap">
{t(i18n.strings.headings.sample)}
</Table.HeadCell>
<Table.HeadCell className="w-48 min-w-48 whitespace-nowrap">
{t(featureAttributeTypeLabel)}
</Table.HeadCell>
<Table.HeadCell className="w-[1%] whitespace-nowrap text-center">
<div className="flex items-center gap-2">
{options.time_series
? t(i18n.strings.headings.timeFeature)
: t(i18n.strings.headings.timeSeries)}
<ToggleInput
onChange={onChangeTimeSeries}
checked={options.time_series || false}
/>
</div>
</Table.HeadCell>
<Table.HeadCell className="w-[1%] whitespace-nowrap text-center">
{t(i18n.strings.headings.configuration)}
</Table.HeadCell>
</>
);
};

type FeatureFieldsProps = {
feature: string;
feature: string | undefined;
} & Pick<
FeaturesAttributesRowsProps,
| "activeFeatureAtom"
| "paramsAtom"
| "loading"
| "optionsAtom"
| "paramsAtom"
| "runRequiredAtom"
| "timeFeatureAtom"
>;
const FeatureFields: FC<FeatureFieldsProps> = ({
activeFeatureAtom,
runRequiredAtom,
paramsAtom,
feature,
loading,
optionsAtom,
paramsAtom,
runRequiredAtom,
timeFeatureAtom,
}) => {
const { t } = useTranslation(i18n.namespace);
Expand All @@ -162,9 +197,13 @@ const FeatureFields: FC<FeatureFieldsProps> = ({
const [params, setParams] = useAtom(paramsAtom);
const options = useAtomValue(optionsAtom);

const attributes = params.features?.[feature];
const attributes = feature ? params.features?.[feature] : undefined;
const setFeatureType = useCallback(
(attributes: FeatureAttributes) => {
if (!feature) {
return;
}

const newParams = setInferFeatureAttributeParamsFeatureAttributes(
params,
feature,
Expand All @@ -175,25 +214,34 @@ const FeatureFields: FC<FeatureFieldsProps> = ({
},
[setRunRequired, setParams, params, feature],
);
const issues = getFeatureAttributeConfigurationIssues(attributes, {
purposes,
});
const issues =
attributes &&
getFeatureAttributeConfigurationIssues(attributes, {
purposes,
});

return (
<Table.Row className="bg-white dark:border-gray-700 dark:bg-gray-800">
<Table.Cell className="min-w-32 text-gray-900 [overflow-wrap:anywhere] dark:text-white">
<span>{feature}</span>
<span>
{loading ? <Skeleton variant="text" className="w-32" /> : feature}
</span>
</Table.Cell>
<Table.Cell className="whitespace-nowrap">
<div className="max-w-52 truncate">
<FeatureAttributeSample attributes={attributes} />
{loading ? (
<Skeleton variant="text" className="w-24" />
) : (
<FeatureAttributeSample attributes={attributes} />
)}
</div>
</Table.Cell>
<Table.Cell className="w-48">
<FeatureTypeControl
attributes={attributes}
feature={feature}
setAttributes={setFeatureType}
disabled={loading}
/>
</Table.Cell>
<Table.Cell className="w-[1%] text-center">
Expand All @@ -207,7 +255,10 @@ const FeatureFields: FC<FeatureFieldsProps> = ({
</Table.Cell>
<Table.Cell className="w-[1%] whitespace-nowrap text-center">
<div className="flex items-center">
<SecondaryButton onClick={() => setActiveFeature(feature)}>
<SecondaryButton
onClick={() => setActiveFeature(feature || "")}
disabled={loading}
>
{t(i18n.strings.actions.configure)}
</SecondaryButton>

Expand All @@ -230,9 +281,12 @@ const FeatureFields: FC<FeatureFieldsProps> = ({
};

type InlineInputProps = {
feature: string;
feature?: string;
};
type AttributeProps = {
type AttributeProps = Omit<
FieldSelectProps,
"fieldType" | "label" | "helperText" | "onChange"
> & {
attributes: FeatureAttributes | undefined;
setAttributes: (attributes: FeatureAttributes) => void;
};
Expand All @@ -242,6 +296,7 @@ type FeatureTypeControlProps = InlineInputProps & AttributeProps;
const FeatureTypeControl: FC<FeatureTypeControlProps> = ({
attributes,
setAttributes,
...props
}) => {
const form = useForm<FeatureAttributes>({ defaultValues: attributes });

Expand Down Expand Up @@ -269,6 +324,7 @@ const FeatureTypeControl: FC<FeatureTypeControlProps> = ({
onChange={onChange}
label={undefined}
helperText={undefined}
{...props}
/>
</FormProvider>
);
Expand All @@ -277,10 +333,14 @@ const FeatureTypeControl: FC<FeatureTypeControlProps> = ({
// Table row time feature radio
type TimeFeatureControlProps = InlineInputProps &
Pick<FeaturesAttributesRowsProps, "timeFeatureAtom"> &
Pick<AttributeProps, "attributes"> & { disabled?: boolean };
Pick<AttributeProps, "attributes"> & {
loading?: boolean;
disabled?: boolean;
};
const TimeFeatureControl: FC<TimeFeatureControlProps> = ({
attributes,
disabled,
loading,
feature,
timeFeatureAtom,
}) => {
Expand All @@ -301,7 +361,7 @@ const TimeFeatureControl: FC<TimeFeatureControlProps> = ({
setTimeFeature(feature);
}}
checked={attributes?.time_series?.time_feature ?? false}
disabled={disabled}
disabled={loading || disabled}
/>
);
};
Expand Down Expand Up @@ -415,25 +475,35 @@ const Form: FC<ConfigureModalProps & { onClose: () => void }> = ({
);
};

type ControlsProps = Pick<FeaturesAttributesRowsProps, "paramsAtom"> & {
type ControlsProps = Pick<
FeaturesAttributesRowsProps,
"loading" | "paramsAtom"
> & {
containerProps: React.HTMLProps<HTMLDivElement>;
};
const Controls: FC<ControlsProps> = ({ paramsAtom, containerProps }) => {
const Controls: FC<ControlsProps> = ({
loading,
paramsAtom,
containerProps,
}) => {
return (
<footer
{...containerProps}
className={twMerge(containerProps.className, "flex gap-2")}
>
<MapDependenciesControl paramsAtom={paramsAtom} />
<MapDependenciesControl loading={loading} paramsAtom={paramsAtom} />
</footer>
);
};

type MapDependenciesControlProps = Pick<
FeaturesAttributesRowsProps,
"paramsAtom"
"loading" | "paramsAtom"
>;
const MapDependenciesControl: FC<MapDependenciesControlProps> = (props) => {
const MapDependenciesControl: FC<MapDependenciesControlProps> = ({
loading,
...props
}) => {
const { t } = useTranslation(i18n.namespace);
const params = useAtomValue(props.paramsAtom);
const featuresAttributes = params.features || {};
Expand All @@ -451,7 +521,7 @@ const MapDependenciesControl: FC<MapDependenciesControlProps> = (props) => {

return (
<>
<SecondaryButton onClick={onOpen} disabled={!features.length}>
<SecondaryButton onClick={onOpen} disabled={loading || !features.length}>
<MapDependentFeatureAttributesIcon className={"mr-1"} />
{label}
</SecondaryButton>
Expand Down

0 comments on commit 2191412

Please sign in to comment.