From 3aaeaca2b031b6e99a8ff88fd3fc503679663923 Mon Sep 17 00:00:00 2001 From: Ananya_Agarwal Date: Tue, 9 Jul 2024 20:07:53 +0530 Subject: [PATCH 1/3] Configuration page in ReactJS Done till before bringing config in alignment styling changes fixing checks newly added WIP WIP WIP Renaming Linting fixed WIP WIP WIP WIP WIP WIP WIP WIP hey WIP WIP WIP WIP wIP WIP WIP --- .../src/beeswax/templates/configuration.mako | 99 -------- .../apps/about/components/ko.hueConfigTree.js | 217 ------------------ .../desktop/js/apps/admin/AdminHeader.scss | 47 ++++ .../js/apps/admin/AdminHeader.test.tsx | 125 ++++++++++ .../src/desktop/js/apps/admin/AdminHeader.tsx | 80 +++++++ .../admin/Configuration/Configuration.scss | 90 ++++++++ .../admin/Configuration/ConfigurationKey.tsx | 63 +++++ .../Configuration/ConfigurationTab.test.tsx | 195 ++++++++++++++++ .../admin/Configuration/ConfigurationTab.tsx | 153 ++++++++++++ .../Configuration/ConfigurationValue.tsx | 41 ++++ .../js/apps/admin/Metrics/Metrics.scss | 20 +- .../{Metrics.test.tsx => MetricsTab.test.tsx} | 9 +- .../Metrics/{Metrics.tsx => MetricsTab.tsx} | 52 ++--- desktop/core/src/desktop/js/ko/ko.all.js | 1 - .../src/desktop/js/reactComponents/imports.js | 5 +- .../src/desktop/templates/dump_config.mako | 31 +-- 16 files changed, 826 insertions(+), 402 deletions(-) delete mode 100644 apps/beeswax/src/beeswax/templates/configuration.mako delete mode 100644 desktop/core/src/desktop/js/apps/about/components/ko.hueConfigTree.js create mode 100644 desktop/core/src/desktop/js/apps/admin/AdminHeader.scss create mode 100644 desktop/core/src/desktop/js/apps/admin/AdminHeader.test.tsx create mode 100644 desktop/core/src/desktop/js/apps/admin/AdminHeader.tsx create mode 100644 desktop/core/src/desktop/js/apps/admin/Configuration/Configuration.scss create mode 100644 desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationKey.tsx create mode 100644 desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationTab.test.tsx create mode 100644 desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationTab.tsx create mode 100644 desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationValue.tsx rename desktop/core/src/desktop/js/apps/admin/Metrics/{Metrics.test.tsx => MetricsTab.test.tsx} (93%) rename desktop/core/src/desktop/js/apps/admin/Metrics/{Metrics.tsx => MetricsTab.tsx} (74%) diff --git a/apps/beeswax/src/beeswax/templates/configuration.mako b/apps/beeswax/src/beeswax/templates/configuration.mako deleted file mode 100644 index 30b89f00854..00000000000 --- a/apps/beeswax/src/beeswax/templates/configuration.mako +++ /dev/null @@ -1,99 +0,0 @@ -## Licensed to Cloudera, Inc. under one -## or more contributor license agreements. See the NOTICE file -## distributed with this work for additional information -## regarding copyright ownership. Cloudera, Inc. licenses this file -## to you under the Apache License, Version 2.0 (the -## "License"); you may not use this file except in compliance -## with the License. You may obtain a copy of the License at -## -## http://www.apache.org/licenses/LICENSE-2.0 -## -## Unless required by applicable law or agreed to in writing, software -## distributed under the License is distributed on an "AS IS" BASIS, -## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -## See the License for the specific language governing permissions and -## limitations under the License. -<%! -import sys - -from desktop.views import commonheader, commonfooter - -if sys.version_info[0] > 2: - from django.utils.translation import gettext as _ -else: - from django.utils.translation import ugettext as _ -%> - -<%namespace name="layout" file="layout.mako" /> -<%namespace name="util" file="util.mako" /> - -${ commonheader(_('Settings'), app_name, user, request) | n,unicode } -${layout.menubar(section='configuration')} - -
-
-

${_('Settings')}

-
-

-

- - - - - - - - - % for key, value in configuration.items(): - - - - % endfor - -
${_('Key')}${_('Value')}
${key or ""}${value or ""}
-

-
-
-
- - - -${ commonfooter(request, messages) | n,unicode } diff --git a/desktop/core/src/desktop/js/apps/about/components/ko.hueConfigTree.js b/desktop/core/src/desktop/js/apps/about/components/ko.hueConfigTree.js deleted file mode 100644 index dc58fc554b4..00000000000 --- a/desktop/core/src/desktop/js/apps/about/components/ko.hueConfigTree.js +++ /dev/null @@ -1,217 +0,0 @@ -// Licensed to Cloudera, Inc. under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. Cloudera, Inc. licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import * as ko from 'knockout'; - -import apiHelper from 'api/apiHelper'; -import componentUtils from 'ko/components/componentUtils'; -import I18n from 'utils/i18n'; -import DisposableComponent from 'ko/components/DisposableComponent'; - -export const NAME = 'hue-config-tree'; - -// prettier-ignore -const TEMPLATE = ` - - -
- - - -
- - -
-
-
- ${ I18n('Configuration files located in') } -
-

- ${ I18n('Sections') } -

-
- -
-
-
- -
-
-
-
- - - - - -
- - - ${ I18n('Empty configuration section') } - -
-
-
-
-
- -
-

${ I18n('Installed Applications') }

-
- - - - - - -
-
-
- - -
-`; - -const filterConfig = (config, lowerCaseFilter) => { - if ( - (config.value && config.value.indexOf(lowerCaseFilter) !== -1) || - (!config.is_anonymous && config.key.indexOf(lowerCaseFilter) !== -1) - ) { - return config; - } - - if (config.values) { - const values = []; - config.values.forEach(val => { - const filtered = filterConfig(val, lowerCaseFilter); - if (filtered) { - values.push(filtered); - } - }); - if (values.length) { - return { - key: config.key, - is_anonymous: config.is_anonymous, - help: config.help, - values: values - }; - } - } -}; - -class HueConfigTree extends DisposableComponent { - constructor(params) { - super(); - this.loading = ko.observable(true); - - this.filter = ko.observable().extend({ throttle: 500 }); - - this.errorMessage = ko.observable(); - this.apps = ko.observableArray(); - this.config = ko.observableArray(); - this.configDir = ko.observable(); - this.selectedKey = ko.observable(); - - this.filteredSections = ko.pureComputed(() => { - if (!this.filter()) { - return this.config(); - } - const lowerCaseFilter = this.filter().toLowerCase(); - - const foundConfigs = []; - let selectedFound = false; - this.config().forEach(config => { - const filtered = filterConfig(config, lowerCaseFilter); - if (filtered) { - foundConfigs.push(filtered); - if (this.selectedKey() && this.selectedKey() === filtered.key) { - selectedFound = true; - } - } - }); - - if (!selectedFound) { - if (foundConfigs.length) { - this.selectedKey(foundConfigs[0].key); - } else { - this.selectedKey(undefined); - } - } - - return foundConfigs; - }); - - this.selectedConfig = ko.pureComputed(() => - this.filteredSections().find(section => section.key === this.selectedKey()) - ); - - this.load(); - } - - async load() { - this.loading(true); - this.errorMessage(undefined); - try { - const response = await apiHelper.fetchHueConfigAsync(); - this.config(response.config); - this.configDir(response.conf_dir); - this.apps(response.apps); - this.selectedKey(this.config().length ? this.config()[0].key : undefined); - } catch (err) { - this.errorMessage(err); - } - this.loading(false); - } -} - -componentUtils.registerComponent(NAME, HueConfigTree, TEMPLATE); diff --git a/desktop/core/src/desktop/js/apps/admin/AdminHeader.scss b/desktop/core/src/desktop/js/apps/admin/AdminHeader.scss new file mode 100644 index 00000000000..aabe1b75cff --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/AdminHeader.scss @@ -0,0 +1,47 @@ +// Licensed to Cloudera, Inc. under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. Cloudera, Inc. licenses this file +// to you under the Apache License, Version 2.0 (the +// 'License'); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@import '../../components/styles/variables'; + +.antd.cuix { + .admin-header { + display: flex; + align-items: center; + + .admin-header__select-dropdown{ + border: 1px solid $fluidx-gray-600; + border-radius: $border-radius-base; + background-color: $fluidx-white; + width: 25%; + height: 32px; + } + + .admin-header__input-filter{ + margin: $font-size-sm; + width: 25%; + + input { + box-shadow: none; + -webkit-box-shadow: none; + } + } + + .config__file-location-value { + color: $fluidx-blue-600; + display: block; + } + } +} \ No newline at end of file diff --git a/desktop/core/src/desktop/js/apps/admin/AdminHeader.test.tsx b/desktop/core/src/desktop/js/apps/admin/AdminHeader.test.tsx new file mode 100644 index 00000000000..720f701b450 --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/AdminHeader.test.tsx @@ -0,0 +1,125 @@ +// Licensed to Cloudera, Inc. under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. Cloudera, Inc. licenses this file +// to you under the Apache License, Version 2.0 (the +// 'License'); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import AdminHeader from './AdminHeader'; + +// Mock data for the component +const options = ['Option 1', 'Option 2', 'Option 3']; +const mockOnSelectChange = jest.fn(); +const mockOnFilterChange = jest.fn(); + +test('renders AdminHeader with correct dropdown and input filter', () => { + render( + + ); + + expect(screen.getByText('Option 1')).toBeInTheDocument(); + expect(screen.queryByText('Option 2')).not.toBeInTheDocument(); + expect(screen.queryByText('Option 3')).not.toBeInTheDocument(); + + expect(screen.getByPlaceholderText('Filter data...')).toBeInTheDocument(); +}); + +test('Calls onSelectChange with correct value when a different option is selected from the dropdown', async () => { + render( + + ); + + const selectDropdown = screen.getByTestId('admin-header--select').firstElementChild; + + if (selectDropdown) { + fireEvent.mouseDown(selectDropdown); + } + + const dropdown = document.querySelector('.ant-select'); + + const secondOption = dropdown?.querySelectorAll('.ant-select-item')[1]; + if (secondOption) { + fireEvent.click(secondOption); + } + + expect(mockOnSelectChange).toHaveBeenCalledWith('Option 2'); +}); + +test('Typing in the input filter should trigger a callback', async () => { + render( + + ); + + const inputFilter = screen.getByPlaceholderText('Filter data...'); + + fireEvent.change(inputFilter, { target: { value: 'new filter' } }); + + expect(mockOnFilterChange).toHaveBeenCalledWith('new filter'); +}); + +test('renders configAddress correctly when passed as prop', () => { + const configAddressValue = 'path/to/config'; + + render( + + ); + + expect(screen.getByText('Configuration files location:')).toBeInTheDocument(); + expect(screen.getByText(configAddressValue)).toBeInTheDocument(); +}); + +test('does not render configAddress when not passed as prop', () => { + render( + + ); + + expect(screen.queryByText('Configuration files location:')).not.toBeInTheDocument(); +}); diff --git a/desktop/core/src/desktop/js/apps/admin/AdminHeader.tsx b/desktop/core/src/desktop/js/apps/admin/AdminHeader.tsx new file mode 100644 index 00000000000..5514e99717a --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/AdminHeader.tsx @@ -0,0 +1,80 @@ +// Licensed to Cloudera, Inc. under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. Cloudera, Inc. licenses this file +// to you under the Apache License, Version 2.0 (the +// 'License'); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { Input } from 'antd'; +import Select from 'cuix/dist/components/Select/Select'; +import { SearchOutlined } from '@ant-design/icons'; +import { i18nReact } from '../../utils/i18nReact'; +import './AdminHeader.scss'; + +const { Option } = Select; + +interface AdminHeaderProps { + options: string[]; + selectedValue: string; + onSelectChange: (value: string) => void; + filterValue: string; + onFilterChange: (value: string) => void; + placeholder: string; + configAddress?: string; +} + +const AdminHeader: React.FC = ({ + options, + selectedValue, + onSelectChange, + filterValue, + onFilterChange, + placeholder, + configAddress +}) => { + const { t } = i18nReact.useTranslation(); + return ( +
+ + + } + value={filterValue} + onChange={e => onFilterChange(e.target.value)} + /> + + {configAddress && ( + + {t('Configuration files location:')} + {configAddress} + + )} +
+ ); +}; + +export default AdminHeader; diff --git a/desktop/core/src/desktop/js/apps/admin/Configuration/Configuration.scss b/desktop/core/src/desktop/js/apps/admin/Configuration/Configuration.scss new file mode 100644 index 00000000000..15017465b7e --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/Configuration/Configuration.scss @@ -0,0 +1,90 @@ +// Licensed to Cloudera, Inc. under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. Cloudera, Inc. licenses this file +// to you under the Apache License, Version 2.0 (the +// 'License'); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@import '../../../components/styles/variables'; + +.antd.cuix { + .config-component { + background-color: $fluidx-gray-100; + padding: 24px; + + .config__section-header { + color: $fluidx-gray-700; + } + + .config__main-item { + .config--last-heading, + .config__main-item--heading { + font-size: $font-size-xl; + font-weight: 300; + } + + .config--last-heading { + padding: 4px 0 8px 0; + } + + padding: 16px 0 8px 16px; + border-bottom: solid 1px $fluidx-gray-300; + background-color: $fluidx-white; + } + + .config__main-item .config__child-item .config--last-heading { + font-size: $font-size-base; + } + + .config__child-item { + font-size: $font-size-base; + color: $fluidx-black; + font-weight: 400; + margin-left: 40px; + } + + .config__last-item--help-text { + color: $fluidx-gray-700; + padding: 4px 8px 12px 21px; + } + + .config__default-section--help-text { + padding: 8px 0 16px 0; + display: block; + font-size: $font-size-base; + color: $fluidx-black; + font-weight: 400; + } + + .config__default-value { + color: $fluidx-gray-500; + font-style: italic; + } + + .config__set-value { + color: $fluidx-gray-900; + padding: 0 4px; + font-size: $font-size-sm; + border-radius: 20px; + background-color: $fluidx-gray-200; + border: 1px solid $hue-border-color; + margin-left: 8px; + } + + .config__help-tooltip { + margin-left: 8px; + cursor: pointer; + color: $fluidx-blue-600; + font-size: $font-size-base; + } + } +} \ No newline at end of file diff --git a/desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationKey.tsx b/desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationKey.tsx new file mode 100644 index 00000000000..d74ad59682b --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationKey.tsx @@ -0,0 +1,63 @@ +// Licensed to Cloudera, Inc. under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. Cloudera, Inc. licenses this file +// to you under the Apache License, Version 2.0 (the +// 'License'); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { Tooltip } from 'antd'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { AdminConfigValue } from './ConfigurationTab'; +import './Configuration.scss'; + +export const ConfigurationKey: React.FC<{ record: AdminConfigValue }> = ({ record }) => { + if (record.is_anonymous) { + return ( +
+ Default section + {record.help && ( + + + + )} +
+ ); + } + if (record?.values) { + return ( + + {record.key} + {record.help && ( + + + + )} + + ); + } else { + return ( + + {record.key} + {record.value && {record.value}} +
+ {record.help} + + {record.default && ( + Default: {record.default} + )} + +
+
+ ); + } +}; diff --git a/desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationTab.test.tsx b/desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationTab.test.tsx new file mode 100644 index 00000000000..5af3e15021e --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationTab.test.tsx @@ -0,0 +1,195 @@ +// Licensed to Cloudera, Inc. under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. Cloudera, Inc. licenses this file +// to you under the Apache License, Version 2.0 (the +// 'License'); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { render, waitFor, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import Configuration from './ConfigurationTab'; +import { ConfigurationKey } from './ConfigurationKey'; +import { ConfigurationValue } from './ConfigurationValue'; +import ApiHelper from '../../../api/apiHelper'; + +// Mock API call to fetch configuration data +jest.mock('../../../api/apiHelper', () => ({ + fetchHueConfigAsync: jest.fn() +})); + +beforeEach(() => { + jest.clearAllMocks(); + ApiHelper.fetchHueConfigAsync = jest.fn(() => + Promise.resolve({ + apps: [{ name: 'desktop', has_ui: true, display_name: 'Desktop' }], + config: [ + { + help: 'Main configuration section', + key: 'desktop', + is_anonymous: false, + values: [ + { + help: 'Example config help text', + key: 'example.config', + is_anonymous: false, + value: 'Example value' + }, + { + help: 'Another config help text', + key: 'another.config', + is_anonymous: false, + value: 'Another value' + } + ] + } + ], + conf_dir: '/conf/directory' + }) + ); +}); + +describe('Configuration Component', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('Renders Configuration component with fetched data', async () => { + render(); + + await waitFor(() => { + expect(screen.getByText(/Sections/i)).toBeInTheDocument(); + expect(screen.getByText(/Desktop/i)).toBeInTheDocument(); + expect(screen.getByText(/example\.config/i)).toBeInTheDocument(); + expect(screen.getByText(/Example value/i)).toBeInTheDocument(); + expect(ApiHelper.fetchHueConfigAsync).toHaveBeenCalled(); + }); + }); + + test('Filters configuration based on input text', async () => { + render(); + + const filterInput = screen.getByPlaceholderText('Filter in desktop...'); + fireEvent.change(filterInput, { target: { value: 'another' } }); + + await waitFor(() => { + expect(screen.queryByText('example.config')).not.toBeInTheDocument(); + expect(screen.getByText('another.config')).toBeInTheDocument(); + }); + }); + + test('Displays message for empty configuration section', async () => { + (ApiHelper.fetchHueConfigAsync as jest.Mock).mockImplementation(() => + Promise.resolve({ + apps: [{ name: 'desktop', has_ui: true, display_name: 'Desktop' }], + config: [{ help: 'No help available.', key: 'desktop', is_anonymous: false, values: [] }], + conf_dir: '/conf/directory' + }) + ); + + render(); + + await waitFor(() => screen.getByText('Empty configuration section')); + expect(screen.getByText('Empty configuration section')).toBeInTheDocument(); + }); + + describe('ConfigurationKey Component', () => { + test('If the record has further values in it, should display record key, helpText as tooltip', () => { + const mockRecord = { + help: 'This is help text', + key: 'config.key', + is_anonymous: false, + values: [ + { + help: 'Example config help text', + key: 'example.config', + is_anonymous: false, + value: 'Example value' + }, + { + help: 'Another config help text', + key: 'another.config', + is_anonymous: false, + value: 'Another value' + } + ] + }; + + const { getByText, container } = render(); + + expect(getByText(/config.key/i)).toBeInTheDocument(); + const tooltip = container.querySelector('.config__help-tooltip'); + expect(tooltip).toBeInTheDocument(); + }); + }); + + test('If the record has no further values in it, verifies the entire config key state', () => { + const record = { + is_anonymous: false, + key: 'Last Config Key', + value: 'Some Value', + help: 'Help info', + default: 'Default Value' + }; + const { getByText, container } = render(); + + expect(getByText('Last Config Key')).toBeInTheDocument(); + expect(getByText('Some Value')).toBeInTheDocument(); + expect(getByText(/Help info/i)).toBeInTheDocument(); + expect(getByText(/Default: Default Value/i)).toBeInTheDocument(); + + const tooltip = container.querySelector('.config__help-tooltip'); + expect(tooltip).not.toBeInTheDocument(); + }); + + test('renders "Default section" and help text as tooltip for anonymous records', () => { + const record = { is_anonymous: true, help: 'Help info', key: 'config.key' }; + const { getByText, container } = render(); + + expect(getByText(/Default section/i)).toBeInTheDocument(); + + const tooltip = container.querySelector('.config__help-tooltip'); + expect(tooltip).toBeInTheDocument(); + }); + + describe('ConfigurationValue Component', () => { + test('If the record has further values in it, renders nested configuration key and values correctly', () => { + const mockRecord = { + help: '', + key: 'parent.config', + is_anonymous: false, + values: [ + { + help: 'child config help', + key: 'child.config', + is_anonymous: false, + value: 'child value' + } + ] + }; + + render(); + + expect(screen.getByText('child.config')).toBeInTheDocument(); + expect(screen.getByText('child value')).toBeInTheDocument(); + expect(screen.queryByText('parent.config')).not.toBeInTheDocument(); + }); + }); + + test('If the record has no further values in it, renders nothing', () => { + const mockRecord = { help: '', key: 'empty.config', is_anonymous: false }; + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationTab.tsx b/desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationTab.tsx new file mode 100644 index 00000000000..810c75f01b0 --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationTab.tsx @@ -0,0 +1,153 @@ +// Licensed to Cloudera, Inc. under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. Cloudera, Inc. licenses this file +// to you under the Apache License, Version 2.0 (the +// 'License'); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React, { useState, useEffect, useMemo } from 'react'; +import { Spin, Alert } from 'antd'; +import { i18nReact } from '../../../utils/i18nReact'; +import AdminHeader from '../AdminHeader'; +import { ConfigurationValue } from './ConfigurationValue'; +import { ConfigurationKey } from './ConfigurationKey'; +import ApiHelper from '../../../api/apiHelper'; +import './Configuration.scss'; + +interface App { + name: string; + has_ui: boolean; + display_name: string; +} + +export interface AdminConfigValue { + help: string; + key: string; + is_anonymous: boolean; + values?: AdminConfigValue[]; + default?: string; + value?: string; +} + +interface Config { + help: string; + key: string; + is_anonymous: boolean; + values: AdminConfigValue[]; +} + +interface HueConfig { + apps: App[]; + config: Config[]; + conf_dir: string; +} + +const Configuration: React.FC = (): JSX.Element => { + const { t } = i18nReact.useTranslation(); + const [hueConfig, setHueConfig] = useState(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(); + const [selectedApp, setSelectedApp] = useState('desktop'); + const [filter, setFilter] = useState(''); + + useEffect(() => { + ApiHelper.fetchHueConfigAsync() + .then(data => { + setHueConfig(data); + if (data.apps.find(app => app.name === 'desktop')) { + setSelectedApp('desktop'); + } + }) + .catch(error => { + setError(error.message); + }) + .finally(() => { + setLoading(false); + }); + }, []); + + const filterConfig = ( + config: AdminConfigValue, + lowerCaseFilter: string + ): AdminConfigValue | undefined => { + //Filtering is done on Key and Help only + const keyMatches = config.key?.toLowerCase().includes(lowerCaseFilter); + const helpMatches = config.help?.toLowerCase().includes(lowerCaseFilter); + + if (keyMatches || helpMatches) { + return config; + } + + if (config.values) { + const filteredValues = config.values + .map(val => filterConfig(val, lowerCaseFilter)) + .filter(Boolean) as AdminConfigValue[]; + if (filteredValues.length) { + return { ...config, values: filteredValues }; + } + } + return undefined; + }; + + const selectedConfig = useMemo(() => { + const filterSelectedApp = hueConfig?.config?.find(config => config.key === selectedApp); + + return filterSelectedApp?.values + .map(config => filterConfig(config, filter.toLowerCase())) + .filter(Boolean) as Config[]; + }, [hueConfig, filter, selectedApp]); + + return ( +
+ + {error && ( + + )} + + {!error && ( + <> +
Sections
+ app.name) || []} + selectedValue={selectedApp} + onSelectChange={setSelectedApp} + filterValue={filter} + onFilterChange={setFilter} + placeholder={`Filter in ${selectedApp}...`} + configAddress={hueConfig?.conf_dir} + /> + {selectedApp && + selectedConfig && + (selectedConfig.length > 0 ? ( + <> + {selectedConfig.map((record, index) => ( +
+ + +
+ ))} + + ) : ( + {t('Empty configuration section')} + ))} + + )} +
+
+ ); +}; + +export default Configuration; diff --git a/desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationValue.tsx b/desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationValue.tsx new file mode 100644 index 00000000000..052c5ec1701 --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationValue.tsx @@ -0,0 +1,41 @@ +// Licensed to Cloudera, Inc. under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. Cloudera, Inc. licenses this file +// to you under the Apache License, Version 2.0 (the +// 'License'); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { AdminConfigValue as ParentConfigValue } from './ConfigurationTab'; +import './Configuration.scss'; +import { ConfigurationKey } from './ConfigurationKey'; + +export const ConfigurationValue: React.FC<{ record: ParentConfigValue }> = ({ record }) => { + if (record.values && record.values.length > 0) { + return ( + <> + {record.values.map((value, index) => ( +
+ + +
+ ))} + + ); + } + return <>; +}; diff --git a/desktop/core/src/desktop/js/apps/admin/Metrics/Metrics.scss b/desktop/core/src/desktop/js/apps/admin/Metrics/Metrics.scss index 34ca5a70845..17765a3a9f8 100644 --- a/desktop/core/src/desktop/js/apps/admin/Metrics/Metrics.scss +++ b/desktop/core/src/desktop/js/apps/admin/Metrics/Metrics.scss @@ -18,7 +18,7 @@ .metrics-component.antd.cuix { background-color: $fluidx-gray-100; - padding: 0 24px 24px 24px; + padding: 24px; .metrics-heading { font-size: $font-size-base; @@ -26,16 +26,6 @@ color: $fluidx-gray-900; } - .metrics-filter { - margin: $font-size-sm; - width: 30%; - - input { - box-shadow: none; - -webkit-box-shadow: none; - } - } - .metrics-table { th { width: 30%; @@ -44,12 +34,4 @@ margin-bottom: $font-size-base; } - - .metrics-select { - border: 1px solid $fluidx-gray-600; - border-radius: $border-radius-base; - background-color: $fluidx-white; - min-width: 200px; - height: 32px; - } } diff --git a/desktop/core/src/desktop/js/apps/admin/Metrics/Metrics.test.tsx b/desktop/core/src/desktop/js/apps/admin/Metrics/MetricsTab.test.tsx similarity index 93% rename from desktop/core/src/desktop/js/apps/admin/Metrics/Metrics.test.tsx rename to desktop/core/src/desktop/js/apps/admin/Metrics/MetricsTab.test.tsx index 70c64b2185c..6fac1a213d2 100644 --- a/desktop/core/src/desktop/js/apps/admin/Metrics/Metrics.test.tsx +++ b/desktop/core/src/desktop/js/apps/admin/Metrics/MetricsTab.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { render, waitFor, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; -import Metrics from './Metrics'; +import Metrics from './MetricsTab'; // Mock the API call to return sample metrics data jest.mock('api/utils', () => ({ @@ -73,12 +73,12 @@ describe('Metrics', () => { }); }); - test('selecting a specific metric from the dropdown filters the data using click events', async () => { + test('Selecting a specific metric from the dropdown filters the data using click events', async () => { render(); await waitFor(() => screen.getByText('queries.number')); - const select = screen.getByTestId('metric-select').firstElementChild; + const select = screen.getByTestId('admin-header--select').firstElementChild; if (select) { fireEvent.mouseDown(select); } @@ -89,7 +89,6 @@ describe('Metrics', () => { if (secondOption) { fireEvent.click(secondOption); await waitFor(() => { - // const headings = screen.queryAllByRole('heading', { level: 4 }); const headings = screen.queryAllByText( (_, element) => element?.tagName.toLowerCase() === 'span' && element?.className === 'metrics-heading' @@ -99,7 +98,7 @@ describe('Metrics', () => { } }); - test('ensuring metrics starting with auth, multiprocessing and python.gc are not displayed', async () => { + test('Ensuring metrics starting with auth, multiprocessing and python.gc are not displayed', async () => { jest.clearAllMocks(); jest.mock('api/utils', () => ({ get: jest.fn(() => diff --git a/desktop/core/src/desktop/js/apps/admin/Metrics/Metrics.tsx b/desktop/core/src/desktop/js/apps/admin/Metrics/MetricsTab.tsx similarity index 74% rename from desktop/core/src/desktop/js/apps/admin/Metrics/Metrics.tsx rename to desktop/core/src/desktop/js/apps/admin/Metrics/MetricsTab.tsx index ed6a3714e44..07e350ba5e2 100644 --- a/desktop/core/src/desktop/js/apps/admin/Metrics/Metrics.tsx +++ b/desktop/core/src/desktop/js/apps/admin/Metrics/MetricsTab.tsx @@ -14,15 +14,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect } from 'react'; import MetricsTable, { MetricsResponse, MetricsTableProps } from './MetricsTable'; -import { Spin, Input, Select, Alert } from 'antd'; +import { Spin, Alert } from 'antd'; import { get } from '../../../api/utils'; -import { SearchOutlined } from '@ant-design/icons'; import { i18nReact } from '../../../utils/i18nReact'; -import './Metrics.scss'; +import AdminHeader from '../AdminHeader'; -const { Option } = Select; +import './Metrics.scss'; const Metrics: React.FC = (): JSX.Element => { const [metrics, setMetrics] = useState(); @@ -30,10 +29,9 @@ const Metrics: React.FC = (): JSX.Element => { const [loading, setLoading] = useState(true); const [error, setError] = useState(); const [searchQuery, setSearchQuery] = useState(''); - const [selectedMetric, setSelectedMetric] = useState(''); + const [selectedMetric, setSelectedMetric] = useState('All'); const [showAllTables, setShowAllTables] = useState(true); const [filteredMetricsData, setFilteredMetricsData] = useState([]); - const dropdownRef = useRef(null); useEffect(() => { const fetchData = async () => { @@ -80,11 +78,11 @@ const Metrics: React.FC = (): JSX.Element => { }; const handleMetricChange = (value: string) => { setSelectedMetric(value); - setShowAllTables(value === ''); + setShowAllTables(value === 'All'); }; - const handleFilterInputChange = (e: React.ChangeEvent) => { - setSearchQuery(e.target.value); + const handleFilterInputChange = (filterValue: string) => { + setSearchQuery(filterValue); }; const { t } = i18nReact.useTranslation(); @@ -93,32 +91,14 @@ const Metrics: React.FC = (): JSX.Element => {
{!error && ( - <> - - - } - /> - + )} {error && ( diff --git a/desktop/core/src/desktop/js/ko/ko.all.js b/desktop/core/src/desktop/js/ko/ko.all.js index 499ef2274b3..750a0e2a204 100644 --- a/desktop/core/src/desktop/js/ko/ko.all.js +++ b/desktop/core/src/desktop/js/ko/ko.all.js @@ -173,7 +173,6 @@ import 'ko/components/ko.shareGistModal'; import 'ko/components/ko.sqlColumnsTable'; // TODO: Move to about app when it has it's own webpack entry -import 'apps/about/components/ko.hueConfigTree'; import 'apps/about/components/ko.connectorsConfig'; import 'ko/extenders/ko.maxLength'; diff --git a/desktop/core/src/desktop/js/reactComponents/imports.js b/desktop/core/src/desktop/js/reactComponents/imports.js index 6e232475e64..9baf374ada4 100644 --- a/desktop/core/src/desktop/js/reactComponents/imports.js +++ b/desktop/core/src/desktop/js/reactComponents/imports.js @@ -11,7 +11,10 @@ export async function loadComponent(name) { return (await import('../apps/storageBrowser/StorageBrowserPage/StorageBrowserPage')).default; case 'Metrics': - return (await import('../apps/admin/Metrics/Metrics')).default; + return (await import('../apps/admin/Metrics/MetricsTab')).default; + + case 'Configuration': + return (await import('../apps/admin/Configuration/ConfigurationTab')).default; // Application global components here case 'AppBanner': diff --git a/desktop/core/src/desktop/templates/dump_config.mako b/desktop/core/src/desktop/templates/dump_config.mako index 2648b7356cd..0c6b2eac849 100644 --- a/desktop/core/src/desktop/templates/dump_config.mako +++ b/desktop/core/src/desktop/templates/dump_config.mako @@ -14,33 +14,16 @@ ## See the License for the specific language governing permissions and ## limitations under the License. -<%! -import logging -import sys - -from desktop.views import commonheader, commonfooter - - -LOG = logging.getLogger() -%> - <%namespace name="layout" file="about_layout.mako" /> ${ layout.menubar(section='dump_config') } - - -
- -
- + +
+ +
\ No newline at end of file From 97c9c0eaa55cc2fd6fee2b03a0e3d85599c688ae Mon Sep 17 00:00:00 2001 From: Ananya_Agarwal Date: Fri, 15 Nov 2024 20:46:43 +0530 Subject: [PATCH 2/3] Changed the metrics Filter to show only the desired row after filtering Linting fixed --- .../src/desktop/js/apps/admin/Metrics/MetricsTab.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/desktop/core/src/desktop/js/apps/admin/Metrics/MetricsTab.tsx b/desktop/core/src/desktop/js/apps/admin/Metrics/MetricsTab.tsx index 07e350ba5e2..4411773c3c8 100644 --- a/desktop/core/src/desktop/js/apps/admin/Metrics/MetricsTab.tsx +++ b/desktop/core/src/desktop/js/apps/admin/Metrics/MetricsTab.tsx @@ -60,9 +60,14 @@ const Metrics: React.FC = (): JSX.Element => { return; } - const filteredData = parseMetricsData(metrics).filter(tableData => - tableData.dataSource.some(data => data.name.toLowerCase().includes(searchQuery.toLowerCase())) - ); + const filteredData = parseMetricsData(metrics) + .map(tableData => ({ + ...tableData, + dataSource: tableData.dataSource.filter(data => + data.name.toLowerCase().includes(searchQuery.toLowerCase()) + ) + })) + .filter(tableData => tableData.dataSource.length > 0); setFilteredMetricsData(filteredData); }, [searchQuery, metrics]); From c76e69721250ae747bc4a26ea9107aa6d3a4b082 Mon Sep 17 00:00:00 2001 From: Ananya_Agarwal Date: Thu, 21 Nov 2024 15:40:31 +0530 Subject: [PATCH 3/3] Changes based on review comments --- .../admin/Configuration/Configuration.scss | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/desktop/core/src/desktop/js/apps/admin/Configuration/Configuration.scss b/desktop/core/src/desktop/js/apps/admin/Configuration/Configuration.scss index 15017465b7e..0f906a56d04 100644 --- a/desktop/core/src/desktop/js/apps/admin/Configuration/Configuration.scss +++ b/desktop/core/src/desktop/js/apps/admin/Configuration/Configuration.scss @@ -23,22 +23,22 @@ .config__section-header { color: $fluidx-gray-700; - } + } .config__main-item { + padding: 16px 0 8px 16px; + border-bottom: solid 1px $fluidx-gray-300; + background-color: $fluidx-white; + .config--last-heading, .config__main-item--heading { - font-size: $font-size-xl; - font-weight: 300; + font-size: $font-size-xl; + font-weight: 300; } .config--last-heading { padding: 4px 0 8px 0; } - - padding: 16px 0 8px 16px; - border-bottom: solid 1px $fluidx-gray-300; - background-color: $fluidx-white; } .config__main-item .config__child-item .config--last-heading { @@ -64,7 +64,7 @@ color: $fluidx-black; font-weight: 400; } - + .config__default-value { color: $fluidx-gray-500; font-style: italic; @@ -81,10 +81,10 @@ } .config__help-tooltip { - margin-left: 8px; - cursor: pointer; - color: $fluidx-blue-600; - font-size: $font-size-base; + margin-left: 8px; + cursor: pointer; + color: $fluidx-blue-600; + font-size: $font-size-base; } } -} \ No newline at end of file +}