diff --git a/apps/about/src/about/templates/admin_wizard.mako b/apps/about/src/about/templates/admin_wizard.mako index 6244cc4c9f4..c426485e7a0 100644 --- a/apps/about/src/about/templates/admin_wizard.mako +++ b/apps/about/src/about/templates/admin_wizard.mako @@ -14,306 +14,13 @@ ## See the License for the specific language governing permissions and ## limitations under the License. -<%! -import sys - -from django.urls import reverse - -from metadata.conf import OPTIMIZER, has_optimizer - -from desktop.auth.backend import is_admin -from desktop.conf import has_connectors -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="/about_layout.mako" /> -% if not is_embeddable: - ${ commonheader(_('Quick Start'), "quickstart", user, request, "70px") | n,unicode } -% endif - ${ layout.menubar(section='quick_start') } -
-
-
-

- % if is_admin(user): - ${ _('Quick Start') } - - % endif - - Hue™ - ${ version } - - - - Query. Explore. Repeat. -

- - % if is_admin(user): -
-
- - - -
-
-
-

${ _('Checking current configuration') }

- -
-
- -
-
-
-
-
- -
-

${ _('Add connectors to data services') }

- - % if has_connectors(): - ... ${ _('connectors installed') } - - - % else: - ${ _('Configuration') } -
- ${ _('Documentation') } - % endif -
- -
-
-

${ _('Install some data examples') }

- -
-
- -
-
-

${ _('Create or import users') }

- ${ _('User Admin') } -
- -
-

${ _('Anonymous usage analytics') }

- -
- - % if not is_embeddable: -
-

${ _('Skip wizard next time') }

- -
- % endif - -
-
- -
-
-
-
- ${ _('Back') } - ${ _('Next') } - ${ _('Done') } - ${ _('Hue and the Hue logo are trademarks of Cloudera, Inc.') } -
-
- % else: -
-

- ${ _('Learn more about Hue and SQL Querying on') } https://gethue.com. -
-
- ${ _('Hue and the Hue logo are trademarks of Cloudera, Inc.') } - % if not user.is_authenticated: -
- - ${ _('Sign in now!') } - - % endif -

-
- % endif - -
-
- +
+
-% if is_admin(user): - - - -% endif - -% if not is_embeddable: - ${ commonfooter(request, messages) | n,unicode } -% endif diff --git a/apps/hive/src/hive/tests.py b/apps/hive/src/hive/tests.py index 3ba8d8670a4..eb3f157f8e9 100644 --- a/apps/hive/src/hive/tests.py +++ b/apps/hive/src/hive/tests.py @@ -21,47 +21,3 @@ import aws from desktop.lib.django_test_util import make_logged_in_client - - -@pytest.mark.django_db -def test_config_check(): - with patch('beeswax.hive_site.get_metastore_warehouse_dir') as get_metastore_warehouse_dir: - with patch('aws.s3.s3fs.S3FileSystem._stats') as s3_stat: - with patch('aws.conf.is_enabled') as is_s3_enabled: - reset = aws.conf.AWS_ACCOUNTS.set_for_testing({ - 'default': { - 'region': 'us-east-1', - 'access_key_id': 'access_key_id', - 'secret_access_key': 'secret_access_key' - } - }), - warehouse = 's3a://yingsdx0602/data1/warehouse/tablespace/managed/hive' - get_metastore_warehouse_dir.return_value = warehouse - is_s3_enabled.return_value = True - s3_stat.return_value = Mock( - DIR_MODE=16895, - FILE_MODE=33206, - aclBit=False, - atime=None, - group='', - isDir=True, - mode=16895, - mtime=None, - name='hive', - path='s3a://yingchensdx/data1/warehouse/tablespace/managed/hive/', - size=0, - type='DIRECTORY', - user='' - ) - - try: - cli = make_logged_in_client() - resp = cli.get('/desktop/debug/check_config') - s3_stat.assert_called() - err_msg = 'Failed to access Hive warehouse: %s' % warehouse - if not isinstance(err_msg, bytes): - err_msg = err_msg.encode('utf-8') - assert err_msg not in resp.content, resp - finally: - for old_conf in reset: - old_conf() diff --git a/desktop/core/src/desktop/api2_tests.py b/desktop/core/src/desktop/api2_tests.py index d801781b6ff..47e883aa4b5 100644 --- a/desktop/core/src/desktop/api2_tests.py +++ b/desktop/core/src/desktop/api2_tests.py @@ -931,7 +931,7 @@ class TestCheckConfigAPI: def test_check_config_success(self): with patch('desktop.api2.os.path.realpath') as mock_hue_conf_dir: with patch('desktop.api2._get_config_errors') as mock_get_config_errors: - request = Mock(method='POST') + request = Mock(method='GET') mock_hue_conf_dir.return_value = '/test/hue/conf' mock_get_config_errors.return_value = [ {"name": "Hive", "message": "The application won't work without a running HiveServer2."}, diff --git a/desktop/core/src/desktop/api_public.py b/desktop/core/src/desktop/api_public.py index 53c3c9e1c8a..a65f1ead2e7 100644 --- a/desktop/core/src/desktop/api_public.py +++ b/desktop/core/src/desktop/api_public.py @@ -72,7 +72,7 @@ def download_hue_logs(request): return logs_api.download_hue_logs(django_request) -@api_view(["POST"]) +@api_view(["GET"]) def check_config(request): django_request = get_django_request(request) return desktop_api.check_config(django_request) diff --git a/desktop/core/src/desktop/js/apps/admin/Components/utils.tsx b/desktop/core/src/desktop/js/apps/admin/Components/utils.ts similarity index 67% rename from desktop/core/src/desktop/js/apps/admin/Components/utils.tsx rename to desktop/core/src/desktop/js/apps/admin/Components/utils.ts index f821fd52198..3c4dc5f567b 100644 --- a/desktop/core/src/desktop/js/apps/admin/Components/utils.tsx +++ b/desktop/core/src/desktop/js/apps/admin/Components/utils.ts @@ -15,3 +15,8 @@ // limitations under the License. export const SERVER_LOGS_API_URL = '/api/v1/logs'; +export const CHECK_CONFIG_EXAMPLES_API_URL = '/api/v1/check_config'; +export const ANALYTICS_PREFERENCES_API_URL = '/about/update_preferences'; +export const INSTALL_APP_EXAMPLES_API_URL = '/api/v1/install_app_examples'; +export const INSTALL_AVAILABLE_EXAMPLES_API_URL = '/api/v1/available_app_examples'; +export const HUE_DOCS_CONFIG_URL = 'https://docs.gethue.com/administrator/configuration/'; diff --git a/desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationTab.tsx b/desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationTab.tsx index feaceda37b5..82165937149 100644 --- a/desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationTab.tsx +++ b/desktop/core/src/desktop/js/apps/admin/Configuration/ConfigurationTab.tsx @@ -15,7 +15,8 @@ // limitations under the License. import React, { useState, useEffect, useMemo } from 'react'; -import { Spin, Alert } from 'antd'; +import Loading from 'cuix/dist/components/Loading'; +import Alert from 'cuix/dist/components/Alert'; import { i18nReact } from '../../../utils/i18nReact'; import AdminHeader from '../AdminHeader'; import { ConfigurationValue } from './ConfigurationValue'; @@ -150,7 +151,7 @@ const Configuration: React.FC = (): JSX.Element => { return (
- + {error && ( { ))} )} - +
); }; 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 a3135547d83..af9939779b9 100644 --- a/desktop/core/src/desktop/js/apps/admin/Metrics/MetricsTab.tsx +++ b/desktop/core/src/desktop/js/apps/admin/Metrics/MetricsTab.tsx @@ -16,7 +16,8 @@ import React, { useState, useEffect } from 'react'; import MetricsTable, { MetricsResponse, MetricsTableProps } from './MetricsTable'; -import { Spin, Alert } from 'antd'; +import Loading from 'cuix/dist/components/Loading'; +import Alert from 'cuix/dist/components/Alert'; import { get } from '../../../api/utils'; import { i18nReact } from '../../../utils/i18nReact'; import AdminHeader from '../AdminHeader'; @@ -94,7 +95,7 @@ const Metrics: React.FC = (): JSX.Element => { return (
- + {!error && ( {
))}
- + ); }; diff --git a/desktop/core/src/desktop/js/apps/admin/Metrics/MetricsTable.tsx b/desktop/core/src/desktop/js/apps/admin/Metrics/MetricsTable.tsx index 491f3085c7d..1f5dd4bef39 100644 --- a/desktop/core/src/desktop/js/apps/admin/Metrics/MetricsTable.tsx +++ b/desktop/core/src/desktop/js/apps/admin/Metrics/MetricsTable.tsx @@ -114,7 +114,7 @@ const MetricsTable: React.FC = ({ caption, dataSource }) => { () => dataSource.map(item => ({ ...item, - name: t(metricLabels[item.name]) || item.name + name: metricLabels[item.name] || item.name })), [dataSource] ); diff --git a/desktop/core/src/desktop/js/apps/admin/Overview/Analytics.tsx b/desktop/core/src/desktop/js/apps/admin/Overview/Analytics.tsx new file mode 100644 index 00000000000..b2a4a4dbd6a --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/Overview/Analytics.tsx @@ -0,0 +1,77 @@ +// 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 } from 'react'; +import huePubSub from '../../../utils/huePubSub'; +import { i18nReact } from '../../../utils/i18nReact'; +import Input from 'cuix/dist/components/Input'; +import { post } from '../../../api/utils'; +import { ANALYTICS_PREFERENCES_API_URL } from '../Components/utils'; +import './Overview.scss'; + +interface UpdatePreferences { + status: number; + message?: string; +} + +const Analytics = (): JSX.Element => { + const [collectUsage, setCollectUsage] = useState(false); + const { t } = i18nReact.useTranslation(); + + const saveCollectUsagePreference = async (collectUsage: boolean) => { + const response = await post(ANALYTICS_PREFERENCES_API_URL, { + collect_usage: collectUsage + }); + + if (response.status === 0) { + const successMessage = collectUsage + ? t('Analytics have been activated.') + : t('Analytics have been deactivated.'); + huePubSub.publish('hue.global.info', { message: successMessage }); + } else { + const errorMessage = collectUsage + ? t('Failed to activate analytics.') + : t('Failed to deactivate analytics.'); + huePubSub.publish('hue.global.error', { message: errorMessage }); + } + }; + + const handleCheckboxChange = event => { + const newPreference = event.target.checked; + setCollectUsage(newPreference); + saveCollectUsagePreference(newPreference); + }; + + return ( +
+

{t('Anonymous usage analytics')}

+
+ + +
+
+ ); +}; + +export default Analytics; diff --git a/desktop/core/src/desktop/js/apps/admin/Overview/ConfigStatus.tsx b/desktop/core/src/desktop/js/apps/admin/Overview/ConfigStatus.tsx new file mode 100644 index 00000000000..2b00c3080e0 --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/Overview/ConfigStatus.tsx @@ -0,0 +1,118 @@ +// 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 Loading from 'cuix/dist/components/Loading'; +import Alert from 'cuix/dist/components/Alert'; +import Table from 'cuix/dist/components/Table'; +import useLoadData from '../../../utils/hooks/useLoadData/useLoadData'; +import { CHECK_CONFIG_EXAMPLES_API_URL } from '../Components/utils'; +import { HUE_DOCS_CONFIG_URL } from '../Components/utils'; +import { i18nReact } from '../../../utils/i18nReact'; +import './Overview.scss'; + +interface ConfigError { + name: string; + message: string; +} +interface CheckConfigResponse { + hueConfigDir: string; + configErrors: ConfigError[]; +} + +const ConfigStatus = (): JSX.Element => { + const { t } = i18nReact.useTranslation(); + const { data, loading, error } = useLoadData(CHECK_CONFIG_EXAMPLES_API_URL); + + const columns = [ + { + dataIndex: 'name', + render: name => {name} + }, + { + key: 'details', + render: record => ( +
+ {record.value && ( +

+ {t('Current value')}: {record.value} +

+ )} +

{record.message}

+
+ ) + } + ]; + + const configErrorsExist = Boolean(data?.configErrors?.length); + + return ( +
+ {loading && } + {error && ( + + )} + {!loading && !error && ( + <> +

{t('Checking current configuration')}

+ {data?.hueConfigDir && ( +
+ {t('Configuration files located in: ')} + {data['hueConfigDir']} +
+ )} + + {configErrorsExist && data ? ( + <> + + + {t('Potential misconfiguration detected.')} + {' '} + {t('Fix and restart Hue.')} + + } + type="warning" + className="config__alert-margin" + /> + + `${record.name}-${record.message.slice(1, 50)}`} + pagination={false} + showHeader={false} + /> + + ) : ( + + )} + + )} + + ); +}; + +export default ConfigStatus; diff --git a/desktop/core/src/desktop/js/apps/admin/Overview/Examples.tsx b/desktop/core/src/desktop/js/apps/admin/Overview/Examples.tsx new file mode 100644 index 00000000000..ea06830c01b --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/Overview/Examples.tsx @@ -0,0 +1,139 @@ +// 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 } from 'react'; +import LinkButton from 'cuix/dist/components/Button/LinkButton'; +import { DownloadOutlined } from '@ant-design/icons'; +import Loading from 'cuix/dist/components/Loading'; +import { + INSTALL_APP_EXAMPLES_API_URL, + INSTALL_AVAILABLE_EXAMPLES_API_URL +} from '../Components/utils'; +import { get, post } from '../../../api/utils'; +import huePubSub from '../../../utils/huePubSub'; +import { i18nReact } from '../../../utils/i18nReact'; +import './Overview.scss'; + +const exampleAppsWithData = [ + { id: 'search', name: 'Solr Search', data: ['log_analytics_demo', 'twitter_demo', 'yelp_demo'] } +]; + +const excludedApps = ['notebook']; + +type InstallExamplesResponse = { + status: number; + message?: string; +}; + +const Examples = (): JSX.Element => { + const { t } = i18nReact.useTranslation(); + const [installingAppId, setInstallingAppId] = useState(''); + const [availableApps, setAvailableApps] = useState<{ [key: string]: string }>({}); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchAvailableApps = async () => { + try { + const response = await get(INSTALL_AVAILABLE_EXAMPLES_API_URL, {}); + if (response.apps) { + const filteredApps = Object.entries(response.apps) + .filter(([appId]) => !excludedApps.includes(appId)) + .reduce((apps, [appId, appName]) => ({ ...apps, [appId]: appName }), {}); + + setAvailableApps(filteredApps); + } + } catch (error) { + console.error('Error fetching available app examples:', error); + } finally { + setLoading(false); + } + }; + + fetchAvailableApps(); + }, []); + + const installExamplesCall = async (url: string, data: Record) => { + const response = await post(url, data); + return response; + }; + + const handleInstall = async (exampleApp: { id: string; name: string }) => { + setInstallingAppId(exampleApp.id); + const url = INSTALL_APP_EXAMPLES_API_URL; + + let actualAppName; + switch (exampleApp.id) { + case 'spark': + actualAppName = 'notebook'; + break; + case 'jobsub': + actualAppName = 'oozie'; + break; + default: + actualAppName = exampleApp.id; + } + + try { + const appWithExtraData = exampleAppsWithData.find(app => app.id === exampleApp.id); + if (appWithExtraData && appWithExtraData.data) { + const installPromises = appWithExtraData.data.map(eachData => + installExamplesCall(url, { + app_name: actualAppName, + data: eachData + }) + ); + await Promise.all(installPromises); + } else { + await installExamplesCall(url, { app_name: actualAppName }); + } + const message = `${actualAppName} ${t('examples installed successfully')}`; + huePubSub.publish('hue.global.info', { message }); + } catch (error) { + const errorMessage = error.message + ? `${t('An error occurred while installing')} ${actualAppName}: ${error.message}` + : `${t('An error occurred while installing')} ${actualAppName}.`; + huePubSub.publish('hue.global.error', { message: errorMessage }); + } finally { + setInstallingAppId(''); + } + }; + + return ( +
+

{t('Install some data examples')}

+ {loading ? ( + + ) : ( + Object.entries(availableApps).map(([appId, appName]) => ( +
+ handleInstall({ id: appId, name: appName })} + disabled={installingAppId === appId} + icon={} + className="examples__install-btn" + > + {installingAppId === appId + ? t('Installing...') + : appName[0].toUpperCase() + appName.slice(1)} + +
+ )) + )} +
+ ); +}; + +export default Examples; diff --git a/desktop/core/src/desktop/js/apps/admin/Overview/Overview.scss b/desktop/core/src/desktop/js/apps/admin/Overview/Overview.scss new file mode 100644 index 00000000000..51c71ad19ae --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/Overview/Overview.scss @@ -0,0 +1,87 @@ +// 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 { + .hue-overview-component { + background-color: $fluidx-gray-100; + padding: 24px; + + .config__spin { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + } + + .config__address-value { + color: $fluidx-blue-600; + padding: 2px 4px; + background-color: $fluidx-gray-040; + border: 1px solid $fluidx-gray-400; + border-radius: 2px; + } + + .config__link { + color: $fluidx-blue-600; + cursor: pointer; + } + + .config__alert-margin { + margin: 10px 0; + } + + .config__table-name, .config__table-value { + color: $fluidx-gray-900; + font-size: 12px; + border-radius: 5px; + margin-left: 8px; + padding: 2px 4px; + background-color: $fluidx-gray-040; + border: 1px solid $fluidx-gray-400; + } + + .overview-examples, + .overview-analytics { + height: 100vh; + } + + .examples__install-btn{ + margin: 10px; + } + + .analytics-checkbox-container { + display: flex; + align-items: center; + } + + .analytics__checkbox-icon { + width: auto; + height: auto; + min-height: 0; + } + + .usage__analytics { + margin-left: 8px; + } + + .overview__trademark-text { + text-align: right; + padding: 10px; + } + } +} diff --git a/desktop/core/src/desktop/js/apps/admin/Overview/OverviewTab.test.tsx b/desktop/core/src/desktop/js/apps/admin/Overview/OverviewTab.test.tsx new file mode 100644 index 00000000000..659643c1735 --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/Overview/OverviewTab.test.tsx @@ -0,0 +1,145 @@ +// 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 '@testing-library/jest-dom'; +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import Overview from './OverviewTab'; +import * as hueConfigModule from '../../../config/hueConfig'; +import Examples from './Examples'; +import Analytics from './Analytics'; +import { + INSTALL_APP_EXAMPLES_API_URL, + INSTALL_AVAILABLE_EXAMPLES_API_URL +} from '../Components/utils'; +import { get, post } from '../../../api/utils'; + +jest.mock('../../../api/utils', () => ({ + post: jest.fn(), + get: jest.fn() +})); + +jest.mock('./ConfigStatus', () => () =>
MockedConfigStatusComponent
); + +jest.mock('../../../config/hueConfig', () => ({ + getLastKnownConfig: jest.fn() +})); + +describe('OverviewTab', () => { + beforeEach(() => { + (hueConfigModule.getLastKnownConfig as jest.Mock).mockReturnValue({ + hue_config: { is_admin: true } + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + test('renders the Tabs with the correct tab labels', () => { + render(); + expect(screen.getByText('Config Status')).toBeInTheDocument(); + expect(screen.getByText('Examples')).toBeInTheDocument(); + expect(screen.getByText('Analytics')).toBeInTheDocument(); + }); + + describe('Analytics Component', () => { + test('renders Analytics tab and can interact with the checkbox', async () => { + (post as jest.Mock).mockResolvedValue({ status: 0, message: 'Success' }); + render(); + const checkbox = screen.getByLabelText(/Help improve Hue with anonymous usage analytics./i); + + expect(checkbox).not.toBeChecked(); + await userEvent.click(checkbox); + expect(checkbox).toBeChecked(); + expect(post).toHaveBeenCalledWith('/about/update_preferences', { + collect_usage: true + }); + userEvent.click(checkbox); + await waitFor(() => expect(checkbox).not.toBeChecked()); + expect(post).toHaveBeenCalledWith('/about/update_preferences', { + collect_usage: false + }); + }); + }); + + describe('Examples component', () => { + const availableAppsResponse = { + apps: { + hive: 'Hive', + impala: 'Impala', + search: 'Solr Search' + } + }; + beforeEach(() => { + (get as jest.Mock).mockImplementation(url => { + if (url === INSTALL_AVAILABLE_EXAMPLES_API_URL) { + return Promise.resolve(availableAppsResponse); + } + return Promise.reject(); + }); + + (post as jest.Mock).mockImplementation(() => + Promise.resolve({ status: 0, message: 'Success' }) + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('fetch and display available apps', async () => { + render(); + + await waitFor(() => { + expect(get).toHaveBeenCalledWith(INSTALL_AVAILABLE_EXAMPLES_API_URL, {}); + Object.entries(availableAppsResponse.apps).forEach(([, appName]) => { + expect(screen.getByText(appName)).toBeInTheDocument(); + }); + }); + }); + + test('post call to install apps without data like hive when the install button is clicked', async () => { + render(); + + await waitFor(() => { + const installButton = screen.getByText('Hive'); + userEvent.click(installButton); + expect(post).toHaveBeenCalledWith(INSTALL_APP_EXAMPLES_API_URL, { app_name: 'hive' }); + }); + }); + + test('post call to install Solr Search example and its data when the install button is clicked', async () => { + render(); + + const solrData = ['log_analytics_demo', 'twitter_demo', 'yelp_demo']; + await waitFor(() => screen.getByText('Solr Search')); + const installButton = screen.getByText('Solr Search'); + userEvent.click(installButton); + + await waitFor(() => { + solrData.forEach(dataEntry => { + expect(post).toHaveBeenCalledWith(INSTALL_APP_EXAMPLES_API_URL, { + app_name: 'search', + data: dataEntry + }); + }); + }); + + expect(post).toHaveBeenCalledTimes(solrData.length); + }); + }); +}); diff --git a/desktop/core/src/desktop/js/apps/admin/Overview/OverviewTab.tsx b/desktop/core/src/desktop/js/apps/admin/Overview/OverviewTab.tsx new file mode 100644 index 00000000000..f9f2cf53d30 --- /dev/null +++ b/desktop/core/src/desktop/js/apps/admin/Overview/OverviewTab.tsx @@ -0,0 +1,56 @@ +// 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 { Tabs } from 'antd'; +import Examples from './Examples'; +import ConfigStatus from './ConfigStatus'; +import Analytics from './Analytics'; +import { i18nReact } from '../../../utils/i18nReact'; +import './Overview.scss'; + +const Overview = (): JSX.Element => { + const { t } = i18nReact.useTranslation(); + + const items = [ + { + label: t('Config Status'), + key: '1', + children: + }, + { + label: t('Examples'), + key: '2', + children: + }, + { + label: t('Analytics'), + key: '3', + children: + } + ]; + + return ( +
+ +
+ {t('Hue and the Hue logo are trademarks of Cloudera, Inc.')} +
+
+ ); +}; + +export default Overview; diff --git a/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsHeader.scss b/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsHeader.scss index 372016113fc..cf209a3149f 100644 --- a/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsHeader.scss +++ b/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsHeader.scss @@ -58,6 +58,9 @@ .server__checkbox-icon { margin-right: 2px; + width: auto; + height: auto; + min-height: 0; } .server__download-button { diff --git a/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsHeader.tsx b/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsHeader.tsx index b0d983ef81c..e1d0f10704f 100644 --- a/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsHeader.tsx +++ b/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsHeader.tsx @@ -15,7 +15,7 @@ // limitations under the License. import React, { useState } from 'react'; -import { Input, Checkbox } from 'antd'; +import Input from 'cuix/dist/components/Input'; import Button from 'cuix/dist/components/Button'; import Search from '@cloudera/cuix-core/icons/react/SearchIcon'; import Download from '@cloudera/cuix-core/icons/react/DownloadIcon'; @@ -62,8 +62,9 @@ const ServerLogsHeader: React.FC = ({ />
- {t(`Host: ${hostName}`)} - {`${t('Host:')} ${hostName}`} + { setWrapLogs(e.target.checked); onWrapLogsChange(e.target.checked); diff --git a/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsTab.tsx b/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsTab.tsx index 044d2f8b5cd..d5162836aca 100644 --- a/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsTab.tsx +++ b/desktop/core/src/desktop/js/apps/admin/ServerLogs/ServerLogsTab.tsx @@ -15,7 +15,7 @@ // limitations under the License. import React, { useState } from 'react'; -import { Alert } from 'antd'; +import Alert from 'cuix/dist/components/Alert'; import ServerLogsHeader from './ServerLogsHeader'; import { i18nReact } from '../../../utils/i18nReact'; import useLoadData from '../../../utils/hooks/useLoadData/useLoadData'; @@ -43,7 +43,7 @@ const ServerLogs: React.FC = (): JSX.Element => { return (
diff --git a/desktop/core/src/desktop/js/reactComponents/imports.js b/desktop/core/src/desktop/js/reactComponents/imports.js index 23b250ecb6d..c54da492738 100644 --- a/desktop/core/src/desktop/js/reactComponents/imports.js +++ b/desktop/core/src/desktop/js/reactComponents/imports.js @@ -10,6 +10,9 @@ export async function loadComponent(name) { case 'StorageBrowserPage': return (await import('../apps/storageBrowser/StorageBrowserPage')).default; + case 'Overview': + return (await import('../apps/admin/Overview/OverviewTab')).default; + case 'Metrics': return (await import('../apps/admin/Metrics/MetricsTab')).default; diff --git a/desktop/core/src/desktop/static/desktop/js/admin-wizard-inline.js b/desktop/core/src/desktop/static/desktop/js/admin-wizard-inline.js index 4f55611ad6c..8c02214656e 100644 --- a/desktop/core/src/desktop/static/desktop/js/admin-wizard-inline.js +++ b/desktop/core/src/desktop/static/desktop/js/admin-wizard-inline.js @@ -1,179 +1,3 @@ -routie.setPathname('/about'); -var AdminWizardViewModel = function () { - var self = this; - - self.connectors = ko.observableArray(); - self.isInstallingSample = ko.observable(false); - - self.installConnectorDataExample = function (connector, event) { - self.isInstallingSample(true); - $.post("/notebook/install_examples", { - connector: connector.id - }, function (data) { - if (data.message) { - huePubSub.publish('hue.global.info', { message: data.message }); - } - if (data.errorMessage) { - huePubSub.publish('hue.global.error', { message: data.errorMessage }); - } - if (data.status == 0 && $(event.target).data("is-connector")) { - huePubSub.publish('cluster.config.refresh.config'); - } - }).always(function (data) { - self.isInstallingSample(false); - }); - } -}; - -function installConnectorExample() { - var button = $(this); - $(button).button('loading'); - $.post(button.data("sample-url"), function (data) { - if (data.status == 0) { - if (data.message) { - huePubSub.publish('hue.global.info', { message: data.message }); - } else { - huePubSub.publish('hue.global.info', { message: 'Examples refreshed' }); - } - if ($(button).data("is-connector")) { - huePubSub.publish('cluster.config.refresh.config'); - } - } else { - huePubSub.publish('hue.global.error', { message: data.message }); - } - }) - .always(function (data) { - $(button).button('reset'); - }); -} - -$(document).ready(function () { - - var adminWizardViewModel = new AdminWizardViewModel(); - ko.applyBindings(adminWizardViewModel, $('#adminWizardComponents')[0]); - - function checkConfig() { - $.get("/desktop/debug/check_config", function (response) { - $("#check-config-section .spinner").css({ - 'position': 'absolute', - 'top': '-100px' - }); - $("#check-config-section .info").html(response); - $("#check-config-section .info").removeClass('hide'); - }) - .fail(function () { - huePubSub.publish('hue.global.error', { message: 'Check config failed: ' }); - }); - } - - $("[rel='popover']").popover(); - - - $(".installBtn").click(installConnectorExample); - - $(".installAllBtn").click(function () { - var button = $(this); - $(button).button('loading'); - var calls = jQuery.map($(button).data("sample-data"), function (app) { - return $.post($(button).data("sample-url"), { data: app }, function (data) { - if (data.status != 0) { - huePubSub.publish('hue.global.error', { message: data.message }); - } - }); - }); - $.when.apply(this, calls) - .then(function () { - huePubSub.publish('hue.global.info', { message: 'Examples refreshed' }); - }) - .always(function (data) { - $(button).button('reset'); - }); - }); - - var currentStep = "step1"; - - routie({ - "step1": function () { - showStep("step1"); - }, - "step2": function () { - showStep("step2"); - }, - "step3": function () { - showStep("step3"); - }, - "step4": function () { - showStep("step4"); - } - }); - - if (window.location.hash === '') { - checkConfig(); - } - - function showStep(step) { - if (window.location.hash === '#step1') { - checkConfig(); - } - - currentStep = step; - if (step != "step1") { - $("#backBtn").removeClass("disabled"); - } else { - $("#backBtn").addClass("disabled"); - } - - if (step != $(".stepDetails:last").attr("id")) { - $("#nextBtn").removeClass("hide"); - $("#doneBtn").addClass("hide"); - } else { - $("#nextBtn").addClass("hide"); - $("#doneBtn").removeClass("hide"); - } - - $("a.step").parent().removeClass("active"); - $("a.step[href='#" + step + "']").parent().addClass("active"); - if (step == "step4") { - $("#lastStep").parent().addClass("active"); - } - $(".stepDetails").hide(); - $("#" + step).show(); - } - - $("#backBtn").click(function () { - var nextStep = (currentStep.substr(4) * 1 - 1); - if (nextStep >= 1) { - routie("step" + nextStep); - } - }); - - $("#nextBtn").click(function () { - var nextStep = (currentStep.substr(4) * 1 + 1); - if (nextStep <= $(".step").length) { - routie("step" + nextStep); - } - }); - - $("#doneBtn").click(function () { - huePubSub.publish('open.link', "/"); - }); - - $(".updatePreferences").click(function () { - $.post("/about/update_preferences", $("input").serialize(), function (data) { - if (data.status == 0) { - huePubSub.publish('hue.global.info', { message: 'Configuration updated' }); - } else { - huePubSub.publish('hue.global.error', { message: data.data }); - } - }); - }); - - $("#updateSkipWizard").prop('checked', $.cookie("hueLandingPage", { path: "/" }) == "home"); - - $("#updateSkipWizard").change(function () { - $.cookie("hueLandingPage", this.checked ? "home" : "wizard", { - path: "/", - secure: window.location.protocol.indexOf('https') > -1 - }); - }); -}); \ No newline at end of file +(function () { + window.createReactComponents('#Overview'); +})(); diff --git a/desktop/core/src/desktop/templates/check_config.mako b/desktop/core/src/desktop/templates/check_config.mako deleted file mode 100644 index 367671a9179..00000000000 --- a/desktop/core/src/desktop/templates/check_config.mako +++ /dev/null @@ -1,65 +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.conf import has_connectors -from desktop.auth.backend import is_hue_admin - -if sys.version_info[0] > 2: - from django.utils.translation import gettext as _ -else: - from django.utils.translation import ugettext as _ -%> - -% if is_hue_admin(user): - ${ _('Configuration files located in') } ${ conf_dir } -% endif - -

- -% if error_list: -
- - ${ _('Potential misconfiguration detected.') } - - % if not has_connectors(): - ${ _('Fix and restart Hue.') } - % endif -
-
-
- % for error in error_list: - - - - - % endfor -
- - ${ error['name'] | n } - - - ## Doesn't make sense to print the value of a BoundContainer - % if 'value' in error: - ${ _('Current value:') } ${ error['value'] }
- % endif - ${ error['message'] | n } -
-% else: -
${ _('All OK. Configuration check passed.') }
-% endif diff --git a/desktop/core/src/desktop/templates/logs.mako b/desktop/core/src/desktop/templates/logs.mako index 97c1c38d0ec..3321a5b600d 100644 --- a/desktop/core/src/desktop/templates/logs.mako +++ b/desktop/core/src/desktop/templates/logs.mako @@ -13,11 +13,6 @@ ## 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 re -import sys -from desktop.views import commonheader, commonfooter -%> <%namespace name="layout" file="about_layout.mako" /> diff --git a/desktop/core/src/desktop/templates/metrics.mako b/desktop/core/src/desktop/templates/metrics.mako index eebe10f54cb..b625a695384 100644 --- a/desktop/core/src/desktop/templates/metrics.mako +++ b/desktop/core/src/desktop/templates/metrics.mako @@ -13,18 +13,9 @@ ## 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 -from desktop import conf -%> <%namespace name="layout" file="about_layout.mako" /> -%if not is_embeddable: -${ commonheader(_('Metrics'), "about", user, request) | n,unicode } -%endif - ${layout.menubar(section='metrics')} @@ -32,7 +23,4 @@ ${layout.menubar(section='metrics')}
- -%if not is_embeddable: -${ commonfooter(request, messages) | n,unicode } -%endif +