Skip to content

Commit ba4f151

Browse files
committed
[ui-config] add search across multiple sections]
1 parent cdafdb6 commit ba4f151

File tree

3 files changed

+154
-30
lines changed

3 files changed

+154
-30
lines changed

desktop/core/src/desktop/js/apps/admin/Configuration/Configuration.scss

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@
2121
background-color: $fluidx-gray-100;
2222
padding: 24px;
2323

24-
.config__section-header {
24+
.config__section-dropdown-label {
2525
color: $fluidx-gray-700;
2626
}
2727

28+
.config__section-header {
29+
margin-top: 16px;
30+
}
31+
2832
.config__main-item {
2933
padding: 16px 0 8px 16px;
3034
border-bottom: solid 1px $fluidx-gray-300;

desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationTab.test.tsx

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import React from 'react';
1818
import { render, waitFor, screen, fireEvent } from '@testing-library/react';
19+
import userEvent from '@testing-library/user-event';
1920
import '@testing-library/jest-dom';
2021
import Configuration from './ConfigurationTab';
2122
import { ConfigurationKey } from './ConfigurationKey';
@@ -31,7 +32,10 @@ beforeEach(() => {
3132
jest.clearAllMocks();
3233
ApiHelper.fetchHueConfigAsync = jest.fn(() =>
3334
Promise.resolve({
34-
apps: [{ name: 'desktop', has_ui: true, display_name: 'Desktop' }],
35+
apps: [
36+
{ name: 'desktop', has_ui: true, display_name: 'Desktop' },
37+
{ name: 'test', has_ui: true, display_name: 'test' }
38+
],
3539
config: [
3640
{
3741
help: 'Main configuration section',
@@ -51,6 +55,19 @@ beforeEach(() => {
5155
value: 'Another value'
5256
}
5357
]
58+
},
59+
{
60+
help: '',
61+
key: 'test',
62+
is_anonymous: false,
63+
values: [
64+
{
65+
help: 'Example config help text2',
66+
key: 'test.config2',
67+
is_anonymous: false,
68+
value: 'Hello World'
69+
}
70+
]
5471
}
5572
],
5673
conf_dir: '/conf/directory'
@@ -63,7 +80,7 @@ describe('Configuration Component', () => {
6380
jest.clearAllMocks();
6481
});
6582

66-
test('Renders Configuration component with fetched data', async () => {
83+
test('Renders Configuration component with fetched data for default desktop section', async () => {
6784
render(<Configuration />);
6885

6986
await waitFor(() => {
@@ -75,6 +92,72 @@ describe('Configuration Component', () => {
7592
});
7693
});
7794

95+
test('Renders Configuration component with fetched data for all sections', async () => {
96+
render(<Configuration />);
97+
98+
await waitFor(() => {
99+
expect(screen.getByText(/Sections/i)).toBeInTheDocument();
100+
expect(screen.getByText(/Desktop/i)).toBeInTheDocument();
101+
expect(screen.getByText(/example\.config/i)).toBeInTheDocument();
102+
expect(screen.getByText(/Example value/i)).toBeInTheDocument();
103+
expect(screen.queryAllByText(/test/i)).toHaveLength(0);
104+
});
105+
106+
const user = userEvent.setup();
107+
108+
// Open dropdown
109+
const select = screen.getByRole('combobox');
110+
await user.click(select);
111+
112+
// Wait for and select "ALL" option
113+
const allOption = await screen.findByTitle('ALL');
114+
await user.click(allOption);
115+
116+
// Verify the updated content
117+
await waitFor(() => {
118+
expect(screen.getAllByText(/test/i)).toHaveLength(3);
119+
expect(screen.getByText(/test\.config2/i)).toBeInTheDocument();
120+
expect(screen.getByText(/Hello World/i)).toBeInTheDocument();
121+
});
122+
});
123+
124+
125+
test('Renders Configuration component mathcing search', async () => {
126+
render(<Configuration />);
127+
128+
await waitFor(() => {
129+
expect(screen.getByText(/Sections/i)).toBeInTheDocument();
130+
expect(screen.getByText(/Desktop/i)).toBeInTheDocument();
131+
expect(screen.getByText(/example\.config/i)).toBeInTheDocument();
132+
expect(screen.getByText(/Example value/i)).toBeInTheDocument();
133+
expect(screen.queryAllByText(/test/i)).toHaveLength(0);
134+
});
135+
136+
const user = userEvent.setup();
137+
138+
// Open dropdown
139+
const select = screen.getByRole('combobox');
140+
await user.click(select);
141+
142+
// Wait for and select "ALL" option
143+
const allOption = await screen.findByTitle('ALL');
144+
await user.click(allOption);
145+
146+
const filterinput = screen.getByPlaceholderText('Filter...');
147+
await user.click(filterinput);
148+
await user.type(filterinput, 'config2');
149+
150+
// Verify the updated content
151+
await waitFor(() => {
152+
expect(screen.getAllByText(/test/i)).toHaveLength(3);
153+
expect(screen.getByText(/test\.config2/i)).toBeInTheDocument();
154+
expect(screen.getByText(/Hello World/i)).toBeInTheDocument();
155+
156+
expect(screen.queryByText(/example\.config/i)).not.toBeInTheDocument();
157+
});
158+
});
159+
160+
78161
test('Filters configuration based on input text', async () => {
79162
render(<Configuration />);
80163

desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationTab.tsx

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -51,20 +51,27 @@ interface HueConfig {
5151
conf_dir: string;
5252
}
5353

54+
interface VisualSection {
55+
header: string;
56+
content: Array<AdminConfigValue>;
57+
}
58+
5459
const Configuration: React.FC = (): JSX.Element => {
5560
const { t } = i18nReact.useTranslation();
5661
const [hueConfig, setHueConfig] = useState<HueConfig>();
5762
const [loading, setLoading] = useState(true);
5863
const [error, setError] = useState<string>();
59-
const [selectedApp, setSelectedApp] = useState<string>('desktop');
64+
const [selectedSection, setSelectedSection] = useState<string>('desktop');
6065
const [filter, setFilter] = useState<string>('');
6166

67+
const ALL_SECTIONS_OPTION = t('ALL');
68+
6269
useEffect(() => {
6370
ApiHelper.fetchHueConfigAsync()
6471
.then(data => {
6572
setHueConfig(data);
6673
if (data.apps.find(app => app.name === 'desktop')) {
67-
setSelectedApp('desktop');
74+
setSelectedSection('desktop');
6875
}
6976
})
7077
.catch(error => {
@@ -98,50 +105,80 @@ const Configuration: React.FC = (): JSX.Element => {
98105
return undefined;
99106
};
100107

101-
const selectedConfig = useMemo(() => {
102-
const filterSelectedApp = hueConfig?.config?.find(config => config.key === selectedApp);
108+
const visualSections = useMemo(() => {
109+
const showAllSections = selectedSection === ALL_SECTIONS_OPTION;
110+
const selectedFullConfigs = !hueConfig?.config
111+
? []
112+
: showAllSections
113+
? hueConfig.config
114+
: hueConfig.config.filter(config => config.key === selectedSection);
115+
116+
return selectedFullConfigs
117+
.map(selectedSection => ({
118+
header: selectedSection.key,
119+
content: selectedSection.values
120+
.map(config => filterConfig(config, filter.toLowerCase()))
121+
.filter(Boolean) as Config[]
122+
}))
123+
.filter(headerContentObj => !!headerContentObj.content) as Array<VisualSection>;
124+
}, [hueConfig, filter, selectedSection]);
125+
126+
const renderVisualSection = (visualSection: VisualSection) => {
127+
const showingMultipleSections = selectedSection === ALL_SECTIONS_OPTION;
128+
const content = visualSection.content;
129+
return content.length > 0 ? (
130+
<>
131+
{showingMultipleSections && (
132+
<h4 className="config__section-header">{visualSection.header}</h4>
133+
)}
134+
{content.map((record, index) => (
135+
<div key={index} className="config__main-item">
136+
<ConfigurationKey record={record} />
137+
<ConfigurationValue record={record} />
138+
</div>
139+
))}
140+
</>
141+
) : (
142+
!showingMultipleSections && <i>{t('Empty configuration section')}</i>
143+
);
144+
};
103145

104-
return filterSelectedApp?.values
105-
.map(config => filterConfig(config, filter.toLowerCase()))
106-
.filter(Boolean) as Config[];
107-
}, [hueConfig, filter, selectedApp]);
146+
const optionsIncludingAll = [
147+
ALL_SECTIONS_OPTION,
148+
...(hueConfig?.apps.map(app => app.name) || [])
149+
];
108150

109151
return (
110152
<div className="config-component">
111153
<Spin spinning={loading}>
112154
{error && (
113155
<Alert
114156
message={`Error: ${error}`}
115-
description="Error in displaying the Configuration!"
157+
description={t('Error in displaying the Configuration!')}
116158
type="error"
117159
/>
118160
)}
119161

120162
{!error && (
121163
<>
122-
<div className="config__section-header">Sections</div>
164+
<div className="config__section-dropdown-label">{t('Sections')}</div>
123165
<AdminHeader
124-
options={hueConfig?.apps.map(app => app.name) || []}
125-
selectedValue={selectedApp}
126-
onSelectChange={setSelectedApp}
166+
options={optionsIncludingAll}
167+
selectedValue={selectedSection}
168+
onSelectChange={setSelectedSection}
127169
filterValue={filter}
128170
onFilterChange={setFilter}
129-
placeholder={`Filter in ${selectedApp}...`}
171+
placeholder={
172+
selectedSection === ALL_SECTIONS_OPTION
173+
? t('Filter...')
174+
: `${t('Filter in')} ${selectedSection}...`
175+
}
130176
configAddress={hueConfig?.conf_dir}
131177
/>
132-
{selectedApp &&
133-
selectedConfig &&
134-
(selectedConfig.length > 0 ? (
135-
<>
136-
{selectedConfig.map((record, index) => (
137-
<div key={index} className="config__main-item">
138-
<ConfigurationKey record={record} />
139-
<ConfigurationValue record={record} />
140-
</div>
141-
))}
142-
</>
143-
) : (
144-
<i>{t('Empty configuration section')}</i>
178+
{selectedSection &&
179+
visualSections?.length &&
180+
visualSections.map(visualSection => (
181+
<div key={visualSection.header}>{renderVisualSection(visualSection)}</div>
145182
))}
146183
</>
147184
)}

0 commit comments

Comments
 (0)