Skip to content

Commit 6aa4c7c

Browse files
ATLAS-5371: Atlas UI: Render ENUM-typed entity attributes as dropdowns in Create/Edit Entity form (#723)
( cherry-picked from the commit 40de707)
1 parent 9a2cd9f commit 6aa4c7c

15 files changed

Lines changed: 1371 additions & 81 deletions
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { LightTooltip } from "@components/muiComponents";
19+
import { Autocomplete, InputLabel, TextField, Typography } from "@mui/material";
20+
import {
21+
areEnumOptionsEqual,
22+
EnumOption,
23+
getEnumOptionLabel,
24+
normalizeMultiEnumValue
25+
} from "@utils/enumTypeUtils";
26+
import { Capitalize, isEmpty } from "@utils/Utils";
27+
import { Controller } from "react-hook-form";
28+
29+
const FormEnumMultiSelect = ({
30+
data,
31+
control,
32+
optionsList,
33+
fieldName
34+
}: {
35+
data: { name: string; isOptional: boolean; typeName: string; cardinality?: string };
36+
control: any;
37+
optionsList: EnumOption[];
38+
fieldName?: string;
39+
}) => {
40+
const { name, isOptional, typeName, cardinality } = data;
41+
42+
return (
43+
<Controller
44+
name={!isEmpty(fieldName) ? `${fieldName}.${name}` : name}
45+
control={control}
46+
key={`enum-multi-${name}`}
47+
rules={{
48+
required: isOptional ? false : true
49+
}}
50+
defaultValue={[]}
51+
render={({ field: { onChange, value }, fieldState: { error } }) => {
52+
const selectedValues = normalizeMultiEnumValue(value, optionsList);
53+
54+
return (
55+
<>
56+
<div className="form-fields">
57+
<InputLabel
58+
className="form-textfield-label"
59+
required={isOptional ? false : true}
60+
>
61+
{Capitalize(name)}
62+
</InputLabel>
63+
<LightTooltip title={`Data Type: (${typeName})`}>
64+
<Typography
65+
color="#666666"
66+
overflow="hidden"
67+
maxWidth="160px"
68+
fontSize={14}
69+
noWrap
70+
>{`(${typeName})${cardinality ? ` ${cardinality}` : ""}`}</Typography>
71+
</LightTooltip>
72+
</div>
73+
<Autocomplete
74+
size="small"
75+
multiple
76+
disableCloseOnSelect
77+
className="form-autocomplete-field"
78+
onChange={(_event, selectedOptions) => {
79+
onChange(selectedOptions);
80+
}}
81+
sx={{ width: "100%" }}
82+
value={selectedValues}
83+
filterSelectedOptions
84+
getOptionLabel={getEnumOptionLabel}
85+
isOptionEqualToValue={areEnumOptionsEqual}
86+
options={optionsList}
87+
renderInput={(params) => (
88+
<TextField
89+
{...params}
90+
error={!!error}
91+
className="form-textfield"
92+
size="small"
93+
InputProps={{
94+
...params.InputProps
95+
}}
96+
placeholder="Select enum values"
97+
/>
98+
)}
99+
/>
100+
</>
101+
);
102+
}}
103+
/>
104+
);
105+
};
106+
107+
export default FormEnumMultiSelect;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import React from "react";
19+
import { render, screen } from "@utils/test-utils";
20+
import userEvent from "@testing-library/user-event";
21+
import { useForm } from "react-hook-form";
22+
import FormEnumMultiSelect from "../FormEnumMultiSelect";
23+
24+
const optionsList = [
25+
{ label: "LRS", value: "LRS" },
26+
{ label: "ZRS", value: "ZRS" },
27+
{ label: "GRS", value: "GRS" }
28+
];
29+
30+
const TestForm = ({
31+
defaultValue = []
32+
}: {
33+
defaultValue?: unknown;
34+
}) => {
35+
const { control } = useForm({
36+
defaultValues: {
37+
replicationTypes: defaultValue
38+
}
39+
});
40+
41+
return (
42+
<FormEnumMultiSelect
43+
data={{
44+
name: "replicationTypes",
45+
isOptional: false,
46+
typeName: "array<adls_gen2_replication>",
47+
cardinality: "SET"
48+
}}
49+
control={control}
50+
optionsList={optionsList}
51+
/>
52+
);
53+
};
54+
55+
describe("FormEnumMultiSelect", () => {
56+
it("renders a multi-select dropdown for array enum attributes", async () => {
57+
const user = userEvent.setup();
58+
render(<TestForm defaultValue={["LRS"]} />);
59+
60+
expect(screen.getByText("ReplicationTypes")).toBeInTheDocument();
61+
expect(screen.getByText("LRS")).toBeInTheDocument();
62+
63+
const valueInput = screen.getByRole("combobox");
64+
await user.click(valueInput);
65+
expect(await screen.findByRole("option", { name: "ZRS" })).toBeInTheDocument();
66+
expect(screen.getByRole("option", { name: "GRS" })).toBeInTheDocument();
67+
});
68+
69+
it("renders empty selection when no default value is provided", () => {
70+
render(<TestForm />);
71+
72+
expect(screen.getByPlaceholderText("Select enum values")).toBeInTheDocument();
73+
});
74+
75+
it("allows optional multi enum to remain empty (negative validation path)", () => {
76+
const OptionalForm = () => {
77+
const { control } = useForm({
78+
defaultValues: {
79+
storageTypes: []
80+
}
81+
});
82+
83+
return (
84+
<FormEnumMultiSelect
85+
data={{
86+
name: "storageTypes",
87+
isOptional: true,
88+
typeName: "array<ozone_storage_type>",
89+
cardinality: "SET"
90+
}}
91+
control={control}
92+
optionsList={[
93+
{ label: "SSD", value: "SSD" },
94+
{ label: "DISK", value: "DISK" }
95+
]}
96+
/>
97+
);
98+
};
99+
100+
render(<OptionalForm />);
101+
expect(screen.getByPlaceholderText("Select enum values")).toBeInTheDocument();
102+
});
103+
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import {
19+
filterEditableEntityTypes,
20+
isEntityTypeEditable,
21+
parseEditableEntityTypes
22+
} from "@utils/entityTypeConfigUtils";
23+
24+
describe("entityTypeConfigUtils", () => {
25+
describe("parseEditableEntityTypes", () => {
26+
it("returns wildcard for * config (positive)", () => {
27+
expect(parseEditableEntityTypes("*")).toBe("*");
28+
});
29+
30+
it("parses comma-separated entity types with spaces (positive)", () => {
31+
expect(parseEditableEntityTypes("hdfs_path, enumchecking")).toEqual([
32+
"hdfs_path",
33+
"enumchecking"
34+
]);
35+
});
36+
37+
it("returns empty array for empty, null, or undefined config (negative)", () => {
38+
expect(parseEditableEntityTypes("")).toEqual([]);
39+
expect(parseEditableEntityTypes(null)).toEqual([]);
40+
expect(parseEditableEntityTypes(undefined)).toEqual([]);
41+
});
42+
43+
it("returns empty array for whitespace-only config (negative)", () => {
44+
expect(parseEditableEntityTypes(" ")).toEqual([]);
45+
});
46+
});
47+
48+
describe("isEntityTypeEditable", () => {
49+
it("allows any type when config is wildcard (positive)", () => {
50+
expect(isEntityTypeEditable("enumchecking", "*")).toBe(true);
51+
expect(isEntityTypeEditable("hdfs_path", "*")).toBe(true);
52+
});
53+
54+
it("allows type when listed in comma-separated config (positive)", () => {
55+
expect(isEntityTypeEditable("enumchecking", "hdfs_path,enumchecking")).toBe(
56+
true
57+
);
58+
});
59+
60+
it("denies type when not listed in config (negative)", () => {
61+
expect(isEntityTypeEditable("enumchecking", "hdfs_path")).toBe(false);
62+
expect(isEntityTypeEditable("DataSet", "hdfs_path,enumchecking")).toBe(
63+
false
64+
);
65+
});
66+
67+
it("denies all types when config is empty (negative)", () => {
68+
expect(isEntityTypeEditable("enumchecking", "")).toBe(false);
69+
expect(isEntityTypeEditable("enumchecking", null)).toBe(false);
70+
});
71+
});
72+
73+
describe("filterEditableEntityTypes", () => {
74+
const allTypes = ["hdfs_path", "enumchecking", "DataSet", "__internal"];
75+
76+
it("returns all types for wildcard config (positive)", () => {
77+
expect(filterEditableEntityTypes(allTypes, "*")).toEqual(allTypes);
78+
});
79+
80+
it("returns only configured types for comma-separated config (positive)", () => {
81+
expect(
82+
filterEditableEntityTypes(allTypes, "hdfs_path,enumchecking")
83+
).toEqual(["hdfs_path", "enumchecking"]);
84+
});
85+
86+
it("returns empty list when config is empty (negative)", () => {
87+
expect(filterEditableEntityTypes(allTypes, "")).toEqual([]);
88+
});
89+
90+
it("returns empty list when no types match config (negative)", () => {
91+
expect(filterEditableEntityTypes(allTypes, "unknown_type")).toEqual([]);
92+
});
93+
94+
it("returns empty list when input type list is empty (negative)", () => {
95+
expect(filterEditableEntityTypes([], "hdfs_path,enumchecking")).toEqual(
96+
[]
97+
);
98+
});
99+
});
100+
});

0 commit comments

Comments
 (0)