Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add CheckboxTree component #1495

Open
wants to merge 58 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
e22c7bd
test(ui): added unit test case for enable count
rohanm-crest Nov 15, 2024
f47cbf1
Merge branch 'develop' of https://github.com/splunk/addonfactory-ucc-…
rohanm-crest Nov 20, 2024
d256182
Merge branch 'develop' of https://github.com/splunk/addonfactory-ucc-…
rohanm-crest Nov 21, 2024
9e22d4f
feat(checkboxTree): added the layout for new component for checkbox tree
rohanm-crest Nov 29, 2024
bd7fd13
feat(storybook): added storybook and sample in input service
rohanm-crest Dec 4, 2024
0ae19f7
Merge branch 'develop' of https://github.com/splunk/addonfactory-ucc-…
rohanm-crest Dec 4, 2024
f23a4c0
chore: remove unwanted files
rohanm-crest Dec 4, 2024
ca2a5eb
Merge branch 'develop' into feat/checkbox-tree-component
rohanm-crest Dec 4, 2024
0cb0bb7
test: added jest test case for checkboxtree
rohanm-crest Dec 4, 2024
0b6d995
Merge branch 'feat/checkbox-tree-component' of https://github.com/spl…
rohanm-crest Dec 4, 2024
a8c4f68
update screenshots
srv-rr-github-token Dec 4, 2024
4bba2e1
feat: added UTC and documentation for checkboxtree component
rohanm-crest Dec 9, 2024
a1d0212
Merge branch 'develop' of https://github.com/splunk/addonfactory-ucc-…
rohanm-crest Dec 9, 2024
4a2512c
Merge branch 'develop' into feat/checkbox-tree-component
rohanm-crest Dec 9, 2024
6bc85b8
update screenshots
srv-rr-github-token Dec 9, 2024
b274197
fix: added dependency for the variable in input.conf
rohanm-crest Dec 9, 2024
5d0501d
Merge branch 'feat/checkbox-tree-component' of https://github.com/spl…
rohanm-crest Dec 9, 2024
53a7753
refactor: update the logic for search to collapse/expand on input
rohanm-crest Dec 10, 2024
627510f
update screenshots
srv-rr-github-token Dec 10, 2024
1685dc7
revert(search): remove the search functionality from checkboxtree
rohanm-crest Dec 12, 2024
08ba881
refactor: change styling and storybook
rohanm-crest Dec 13, 2024
e4ccd69
update screenshots
srv-rr-github-token Dec 13, 2024
7f4e6f7
docs: change screenshot for checkboxtree
rohanm-crest Dec 13, 2024
ed56993
Merge branch 'feat/checkbox-tree-component' of https://github.com/spl…
rohanm-crest Dec 13, 2024
0dba61a
revert: delete search screenshot
rohanm-crest Dec 13, 2024
3df9abb
Merge branch 'develop' into feat/checkbox-tree-component
rohanm-crest Dec 13, 2024
b502f45
update screenshots
srv-rr-github-token Dec 13, 2024
4631fb4
feat: add support for disable and prisma
rohanm-crest Dec 18, 2024
aeed161
Merge branch 'feat/checkbox-tree-component' of https://github.com/spl…
rohanm-crest Dec 18, 2024
7611965
update screenshots
srv-rr-github-token Dec 18, 2024
7666043
Merge branch 'develop' into feat/checkbox-tree-component
rohanm-crest Dec 18, 2024
d8b278c
fix: resolve issue for disable in group checkbox
rohanm-crest Dec 18, 2024
ac152cf
update screenshots
srv-rr-github-token Dec 18, 2024
e9fe08e
revert(disable): remove the disable, disableonedit is already present
rohanm-crest Dec 26, 2024
bf69c7e
Merge branch 'feat/checkbox-tree-component' of https://github.com/spl…
rohanm-crest Dec 26, 2024
7fbbbea
update screenshots
srv-rr-github-token Dec 26, 2024
12ea7b6
Merge branch 'develop' of https://github.com/splunk/addonfactory-ucc-…
rohanm-crest Jan 2, 2025
ca81a15
Merge branch 'develop' of https://github.com/splunk/addonfactory-ucc-…
rohanm-crest Jan 3, 2025
31f117c
Merge branch 'develop' of https://github.com/splunk/addonfactory-ucc-…
rohanm-crest Jan 7, 2025
de57812
Merge branch 'develop' into feat/checkbox-tree-component
rohanm-crest Jan 7, 2025
215f3f7
revert: indeterminate state in the checkboxtree
rohanm-crest Jan 8, 2025
77c69ab
Merge branch 'develop' into feat/checkbox-tree-component
rohanm-crest Jan 8, 2025
ec50104
update screenshots
srv-rr-github-token Jan 8, 2025
fd2fa61
fix: style for parent checkboxtree
rohanm-crest Jan 8, 2025
40d3424
Merge branch 'feat/checkbox-tree-component' of https://github.com/spl…
rohanm-crest Jan 8, 2025
556ce96
update screenshots
srv-rr-github-token Jan 8, 2025
a1132c1
trigger ci
vtsvetkov-splunk Jan 8, 2025
df4c5d9
adjust for compact view
vtsvetkov-splunk Jan 8, 2025
a40fd2f
update screenshots
srv-rr-github-token Jan 8, 2025
4ec5210
fix field
vtsvetkov-splunk Jan 8, 2025
6702f4e
wipe all images to regenerate stale
vtsvetkov-splunk Jan 8, 2025
0fa3862
update screenshots
srv-rr-github-token Jan 8, 2025
d0155ef
trigger ci
vtsvetkov-splunk Jan 8, 2025
1d73484
Merge remote-tracking branch 'origin/develop' into feat/checkbox-tree…
vtsvetkov-splunk Jan 8, 2025
c5b363f
eslint fix
vtsvetkov-splunk Jan 8, 2025
f489854
fix path
vtsvetkov-splunk Jan 8, 2025
674410e
fix unit tests
vtsvetkov-splunk Jan 8, 2025
15afc57
fix unit tests
vtsvetkov-splunk Jan 8, 2025
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
109 changes: 109 additions & 0 deletions ui/src/components/CheckboxTree/CheckboxSubTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { useMemo, useState } from 'react';
import CheckboxRowWrapper from './CheckboxTreeRowWrapper';
import { getCheckedCheckboxesCount, GroupWithRows, ValueByField } from './CheckboxTree.utils';
import {
CheckboxContainer,
CheckboxWrapper,
Description,
GroupLabel,
RowContainer,
StyledCollapsiblePanel,
} from './StyledComponent';

interface CheckboxSubTreeProps {
group: GroupWithRows;
values: ValueByField;
handleRowChange: (newValue: { field: string; checkbox: boolean; text?: string }) => void;
disabled?: boolean;
handleParentCheckboxTree: (groupLabel: string, newCheckboxValue: boolean) => void;
}

const CheckboxSubTree: React.FC<CheckboxSubTreeProps> = ({
group,
values,
handleRowChange,
disabled,
handleParentCheckboxTree,
}) => {
const [isExpanded, setIsExpanded] = useState(true);

const isParentChecked = useMemo(
() => group.rows.every((row) => values.get(row.field)?.checkbox),
[group.rows, values]
);

const isIndeterminate = useMemo(
() => group.rows.some((row) => values.get(row.field)?.checkbox) && !isParentChecked,
rohanm-crest marked this conversation as resolved.
Show resolved Hide resolved
[group.rows, values, isParentChecked]
);

const checkedCheckboxesCount = useMemo(
() => getCheckedCheckboxesCount(group, values),
[group, values]
);

const toggleCollapse = () => setIsExpanded((prev) => !prev);

const ParentCheckbox = (
<CheckboxWrapper>
<input
rohanm-crest marked this conversation as resolved.
Show resolved Hide resolved
type="checkbox"
checked={isParentChecked}
ref={(el) => {
const inputElement = el as HTMLInputElement | null;
if (inputElement) {
inputElement.indeterminate = isIndeterminate;
}
}}
onChange={() => handleParentCheckboxTree(group.label, !isParentChecked)}
disabled={disabled}
/>
<span>{group.label}</span>
</CheckboxWrapper>
);

const childRows = (
<RowContainer>
{group.rows.map((row) => (
<CheckboxRowWrapper
key={`row_${row.field}`}
disabled={disabled}
row={row}
values={values}
handleRowChange={handleRowChange}
/>
))}
</RowContainer>
);

const description = (
<Description>
{checkedCheckboxesCount} of {group.fields.length}
</Description>
);

return (
<CheckboxContainer>
{group.options?.isExpandable ? (
<StyledCollapsiblePanel
open={isExpanded}
onChange={toggleCollapse}
title={ParentCheckbox}
actions={description}
>
{childRows}
</StyledCollapsiblePanel>
) : (
<>
<GroupLabel>
{ParentCheckbox}
{description}
</GroupLabel>
{childRows}
</>
)}
</CheckboxContainer>
);
};

export default CheckboxSubTree;
173 changes: 173 additions & 0 deletions ui/src/components/CheckboxTree/CheckboxTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import React, { useEffect, useState, useCallback } from 'react';
import ColumnLayout from '@splunk/react-ui/ColumnLayout';
import Button from '@splunk/react-ui/Button';
import Search from '@splunk/react-ui/Search';
import { StyledColumnLayout } from './StyledComponent';
import {
getDefaultValues,
getFlattenRowsWithGroups,
getNewCheckboxValues,
isGroupWithRows,
} from './CheckboxTree.utils';
import CheckboxSubTree from './CheckboxSubTree';
import CheckboxRowWrapper from './CheckboxTreeRowWrapper';
import { MODE_CREATE } from '../../constants/modes';
import { CheckboxTreeProps, ValueByField } from './types';
import { packValue, parseValue } from './utils';

type SearchChangeData = {
value: string;
};

function CheckboxTree(props: CheckboxTreeProps) {
const { field, handleChange, controlOptions, disabled } = props;
const flattenedRowsWithGroups = getFlattenRowsWithGroups(controlOptions);
rohanm-crest marked this conversation as resolved.
Show resolved Hide resolved
const shouldUseDefaultValue =
props.mode === MODE_CREATE && (props.value === null || props.value === undefined);
const initialValues = shouldUseDefaultValue
? getDefaultValues(controlOptions.rows)
: parseValue(props.value);

const [values, setValues] = useState(initialValues);
const [searchForCheckBoxValue, setSearchForCheckBoxValue] = useState('');

// Propagate default values on mount if applicable
useEffect(() => {
if (shouldUseDefaultValue) {
handleChange(field, packValue(initialValues), 'CheckboxTree');
}
}, [field, handleChange, shouldUseDefaultValue, initialValues]);

const handleRowChange = useCallback(
(newValue: { field: string; checkbox: boolean; text?: string }) => {
setValues((prevValues: ValueByField) => {
const updatedValues = getNewCheckboxValues(prevValues, newValue);
handleChange(field, packValue(updatedValues), 'CheckboxTree');
return updatedValues;
});
},
[field, handleChange]
);

const handleParentCheckboxTree = useCallback(
rohanm-crest marked this conversation as resolved.
Show resolved Hide resolved
(groupLabel: string, newCheckboxValue: boolean) => {
if (!controlOptions?.groups) {
return;
}

const group = controlOptions.groups.find((g) => g.label === groupLabel);
if (!group) {
return;
}

setValues((prevValues) => {
const updatedValues = new Map(prevValues);
group.fields.forEach((item) => {
updatedValues.set(item, { checkbox: newCheckboxValue });
});
handleChange(field, packValue(updatedValues), 'CheckboxTree');
return updatedValues;
});
},
[controlOptions, field, handleChange]
);

const handleCheckboxToggleAll = useCallback(
(newCheckboxValue: boolean) => {
setValues((prevValues) => {
const updatedValues = new Map(prevValues);
controlOptions.rows.forEach((row) => {
updatedValues.set(row.field, { checkbox: newCheckboxValue });
});
handleChange(field, packValue(updatedValues), 'CheckboxTree');
return updatedValues;
});
},
[controlOptions.rows, field, handleChange]
);

const handleSearchChange = useCallback(
(e: React.SyntheticEvent, { value: searchValue }: SearchChangeData) => {
setSearchForCheckBoxValue(searchValue);
},
[]
);

const filterRows = useCallback(() => {
const searchValueLower = searchForCheckBoxValue.toLowerCase();

return flattenedRowsWithGroups
.flatMap((row) => {
if (isGroupWithRows(row)) {
const groupMatches = row.label.toLowerCase().includes(searchValueLower);
const filteredRows = groupMatches
? row.rows
: row.rows.filter((childRow) =>
childRow.checkbox?.label?.toLowerCase().includes(searchValueLower)
);

return groupMatches || filteredRows.length > 0
? { ...row, rows: filteredRows }
: [];
}

const rowMatches = row.checkbox?.label?.toLowerCase().includes(searchValueLower);
return rowMatches ? row : null;
})
.filter(Boolean);
}, [flattenedRowsWithGroups, searchForCheckBoxValue]);

const filteredRows = filterRows();

return (
<>
<Search
style={{ width: '320px', marginBottom: '10px' }}
inline
onChange={handleSearchChange}
value={searchForCheckBoxValue}
/>
<StyledColumnLayout gutter={5}>
{filteredRows.map((row) =>
row && isGroupWithRows(row) ? (
<ColumnLayout.Row key={`group_${row.label}`}>
<CheckboxSubTree
group={row}
values={values}
handleRowChange={handleRowChange}
disabled={disabled}
handleParentCheckboxTree={handleParentCheckboxTree}
/>
</ColumnLayout.Row>
) : (
row && (
<ColumnLayout.Row key={`row_${row.field}`}>
<CheckboxRowWrapper
row={row}
values={values}
handleRowChange={handleRowChange}
disabled={disabled}
/>
</ColumnLayout.Row>
)
)
)}
<ColumnLayout.Row />
</StyledColumnLayout>
<div>
<Button
label="Select All"
appearance="pill"
onClick={() => handleCheckboxToggleAll(true)}
/>
<Button
label="Clear All"
appearance="pill"
onClick={() => handleCheckboxToggleAll(false)}
/>
</div>
</>
);
}

export default CheckboxTree;
73 changes: 73 additions & 0 deletions ui/src/components/CheckboxTree/CheckboxTree.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { CheckboxTreeProps, Field, GroupWithRows, Row, Value, ValueByField } from './types';

export function isGroupWithRows(item: GroupWithRows | Row): item is GroupWithRows {
return 'label' in item;
}

export function getFlattenRowsWithGroups({ groups, rows }: CheckboxTreeProps['controlOptions']) {
const flattenRowsMixedWithGroups: Array<GroupWithRows | Row> = [];

rows.forEach((row) => {
const groupForThisRow = groups?.find((group) => group.fields.includes(row.field));
if (groupForThisRow) {
const addedGroup = flattenRowsMixedWithGroups.find(
(item): item is GroupWithRows =>
isGroupWithRows(item) && item.label === groupForThisRow.label
);
const groupToAdd = addedGroup || {
...groupForThisRow,
rows: [],
};
groupToAdd.rows.push(row);
if (!addedGroup) {
flattenRowsMixedWithGroups.push(groupToAdd);
}
return;
}
flattenRowsMixedWithGroups.push(row);
});

return flattenRowsMixedWithGroups;
}
Comment on lines +7 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fort this one i needed some more time while reading it wdyt about refactoring it into sth like this

Suggested change
export function getFlattenRowsWithGroups({ groups, rows }: CheckboxTreeProps['controlOptions']) {
const flattenRowsMixedWithGroups: Array<GroupWithRows | Row> = [];
rows.forEach((row) => {
const groupForThisRow = groups?.find((group) => group.fields.includes(row.field));
if (groupForThisRow) {
const addedGroup = flattenRowsMixedWithGroups.find(
(item): item is GroupWithRows =>
isGroupWithRows(item) && item.label === groupForThisRow.label
);
const groupToAdd = addedGroup || {
...groupForThisRow,
rows: [],
};
groupToAdd.rows.push(row);
if (!addedGroup) {
flattenRowsMixedWithGroups.push(groupToAdd);
}
return;
}
flattenRowsMixedWithGroups.push(row);
});
return flattenRowsMixedWithGroups;
}
export function getFlattenRowsWithGroups({ groups, rows }: CheckboxTreeProps['controlOptions']) {
const flattenRowsMixedWithGroups: Array<GroupWithRows | Row> = [];
rows.forEach((row) => {
const groupForThisRow = groups?.find((group) => group.fields.includes(row.field));
if (!groupForThisRow) {
// no group needed for this row
flattenRowsMixedWithGroups.push(row);
return;
}
const existingGroup = flattenRowsMixedWithGroups.find(
(item): item is GroupWithRows =>
isGroupWithRows(item) && item.label === groupForThisRow.label
);
if (!existingGroup) {
// add new group
flattenRowsMixedWithGroups.push({
...groupForThisRow,
rows: [row],
});
return;
}
// add to existing group
existingGroup.rows.push(row);
});
return flattenRowsMixedWithGroups;
}

or even something like this

Suggested change
export function getFlattenRowsWithGroups({ groups, rows }: CheckboxTreeProps['controlOptions']) {
const flattenRowsMixedWithGroups: Array<GroupWithRows | Row> = [];
rows.forEach((row) => {
const groupForThisRow = groups?.find((group) => group.fields.includes(row.field));
if (groupForThisRow) {
const addedGroup = flattenRowsMixedWithGroups.find(
(item): item is GroupWithRows =>
isGroupWithRows(item) && item.label === groupForThisRow.label
);
const groupToAdd = addedGroup || {
...groupForThisRow,
rows: [],
};
groupToAdd.rows.push(row);
if (!addedGroup) {
flattenRowsMixedWithGroups.push(groupToAdd);
}
return;
}
flattenRowsMixedWithGroups.push(row);
});
return flattenRowsMixedWithGroups;
}
export function getFlattenRowsWithGroups({ groups, rows }: CheckboxTreeProps['controlOptions']) {
return rows.reduce<Array<GroupWithRows | Row>>((flattenRowsMixedWithGroups, row) => {
const groupForThisRow = groups?.find((group) => group.fields.includes(row.field));
if (!groupForThisRow) {
// no group needed for this row
return [...flattenRowsMixedWithGroups, row];
}
const existingGroup = flattenRowsMixedWithGroups.find(
(item): item is GroupWithRows =>
isGroupWithRows(item) && item.label === groupForThisRow.label
);
if (!existingGroup) {
// add new group
return [
...flattenRowsMixedWithGroups,
{
...groupForThisRow,
rows: [row],
},
];
}
// add to existing group
existingGroup.rows.push(row);
return flattenRowsMixedWithGroups;
}, []);
}


export function getNewCheckboxValues(
values: ValueByField,
newValue: {
field: string;
checkbox: boolean;
}
) {
const newValues = new Map(values);
newValues.set(newValue.field, {
checkbox: newValue.checkbox,
});

return newValues;
}

export function getCheckedCheckboxesCount(group: GroupWithRows, values: ValueByField) {
let checkedCheckboxesCount = 0;
group.rows.forEach((row) => {
if (values.get(row.field)?.checkbox) {
checkedCheckboxesCount += 1;
}
});
return checkedCheckboxesCount;
}

export function getDefaultValues(rows: Row[]): ValueByField {
const resultMap = new Map<Field, Value>();

rows.forEach((row) => {
if (!isGroupWithRows(row)) {
const checkboxDefaultValue = row.checkbox?.defaultValue;
if (typeof checkboxDefaultValue === 'boolean') {
resultMap.set(row.field, {
checkbox: checkboxDefaultValue,
});
}
}
});
rohanm-crest marked this conversation as resolved.
Show resolved Hide resolved

return resultMap;
}
37 changes: 37 additions & 0 deletions ui/src/components/CheckboxTree/CheckboxTreeRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import { StyledRow, StyledSwitch } from './StyledComponent';

interface CheckboxRowProps {
field: string;
label: string;
checkbox: boolean;
disabled?: boolean;
handleChange: (value: { field: string; checkbox: boolean }) => void;
}

function CheckboxRow(props: CheckboxRowProps) {
const { field, label, checkbox, disabled, handleChange } = props;
const handleChangeCheckbox = (
event: React.MouseEvent<HTMLElement>,
data: { selected: boolean }
) => {
handleChange({ field, checkbox: !data.selected });
};

return (
<StyledRow>
<StyledSwitch
aria-label={`${label} checkbox`}
data-test-field={field}
selected={checkbox}
onClick={handleChangeCheckbox}
appearance="checkbox"
disabled={disabled}
>
{label}
</StyledSwitch>
</StyledRow>
);
}

export default CheckboxRow;
Loading
Loading