Skip to content

Commit

Permalink
[frontend] Move the banner into a client side react component
Browse files Browse the repository at this point in the history
This also reconsolidates multiple login related styles into one .less file.
  • Loading branch information
JohanAhlen committed Dec 14, 2022
1 parent f4a36e2 commit eb8b264
Show file tree
Hide file tree
Showing 17 changed files with 362 additions and 246 deletions.
1 change: 0 additions & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ module.exports = function(grunt) {
'desktop/core/src/desktop/static/desktop/css/hue.css': 'desktop/core/src/desktop/static/desktop/less/hue.less',
'desktop/core/src/desktop/static/desktop/css/hue3-extra.css': 'desktop/core/src/desktop/static/desktop/less/hue3-extra.less',
'desktop/core/src/desktop/static/desktop/css/login.css': 'desktop/core/src/desktop/static/desktop/less/login.less',
'desktop/core/src/desktop/static/desktop/css/login4.css': 'desktop/core/src/desktop/static/desktop/less/login4.less',
'desktop/core/src/desktop/static/desktop/css/httperrors.css': 'desktop/core/src/desktop/static/desktop/less/httperrors.less',
'apps/metastore/src/metastore/static/metastore/css/metastore.css': 'apps/metastore/src/metastore/static/metastore/less/metastore.less',
'desktop/libs/notebook/src/notebook/static/notebook/css/notebook.css': 'desktop/libs/notebook/src/notebook/static/notebook/less/notebook.less',
Expand Down
1 change: 1 addition & 0 deletions desktop/core/src/desktop/js/api/urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
// limitations under the License.

export const AUTOCOMPLETE_API_PREFIX = '/api/editor/autocomplete/';
export const BANNERS_API = '/api/banners/';
export const SAMPLE_API_PREFIX = '/notebook/api/sample/';
export const EXECUTE_API_PREFIX = '/api/editor/execute/'; // Dups with api.ts
export const DOCUMENTS_API = '/desktop/api2/doc/';
Expand Down
2 changes: 2 additions & 0 deletions desktop/core/src/desktop/js/hue.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ window.createReactComponents = createReactComponents;
$(document).ready(async () => {
await refreshConfig(); // Make sure we have config up front

createReactComponents('.main-page');

const onePageViewModel = new OnePageViewModel();
ko.applyBindings(onePageViewModel, $('.page-content')[0]);

Expand Down
3 changes: 3 additions & 0 deletions desktop/core/src/desktop/js/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ window.huePubSub = huePubSub;

import { createApp } from 'vue';
import TrademarkBanner from 'vue/components/login/TrademarkBanner.vue';
import { createReactComponents } from './reactComponents/createRootElements';

window.addEventListener('DOMContentLoaded', () => {
createReactComponents('.login-page');

createApp({
components: {
'trademark-banner': TrademarkBanner
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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/colors.scss';
@import '../../components/styles/mixins.scss';

.app-banner {
@include flex(0 0 auto);
}

.app-banner--system {
padding: 4px;
text-align: center;
background-color: $fluid-blue-800;
color: $fluid-blue-050;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// 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, screen } from '@testing-library/react';
import '@testing-library/jest-dom';

import AppBanner from './AppBanner';
import { CancellablePromise } from '../../api/cancellablePromise';
import * as ApiUtils from '../../api/utils';

describe('AppBanner', () => {
let apiMock;

const setupMock = (configured?: string, system?: string) => {
apiMock = jest
.spyOn<ApiUtils, ApiUtils['get']>(ApiUtils, 'get')
.mockReturnValue(CancellablePromise.resolve({ configured, system }));
};

afterEach(() => {
apiMock?.mockClear();
});

test('it should show a configured banner', async () => {
const configuredBanner = '<div>Configured text <a href="some">Link</a></div>';
setupMock(configuredBanner);

render(<AppBanner />);

expect((await screen.findByText(/Configured/))?.outerHTML).toEqual(configuredBanner);
});

test('it should show a sanitized configured banner', async () => {
const configuredBanner =
'<div>Configured text <a href="some">Link</a><script>alert("xss");</script></div>';
const expectedBanner = '<div>Configured text <a href="some">Link</a></div>';
setupMock(configuredBanner);

render(<AppBanner />);

expect((await screen.findByText(/Configured/))?.outerHTML).toEqual(expectedBanner);
});

test('it should show a configured banner with sanitized styles', async () => {
const configuredBanner =
'<div style="color: #aabbcc; width: expression(alert(\'XSS\'));font-size:1px;">Configured text</div>';
const expectedBanner = '<div style="color:#aabbcc;font-size:1px">Configured text</div>';
setupMock(configuredBanner);

render(<AppBanner />);

expect((await screen.findByText(/Configured/))?.outerHTML).toEqual(expectedBanner);
});

test('it should show a system banner', async () => {
const systemBanner = '<div>System text</div>';
setupMock(undefined, systemBanner);

render(<AppBanner />);

expect((await screen.findByText(/System/))?.outerHTML).toEqual(systemBanner);
});

test('it should show a system banner instead of configured if both are present', async () => {
const configuredBanner = '<div>Configured text <a href="some">Link</a></div>';
const systemBanner = '<div>System text</div>';
setupMock(configuredBanner, systemBanner);

render(<AppBanner />);

expect(await screen.findByText(/System/)).toBeInTheDocument();
expect(screen.queryByText(/Configured/)).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// 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, { useEffect, useState } from 'react';
import sanitizeHtml, { IOptions } from 'sanitize-html';

import './AppBanner.scss';
import { BANNERS_API } from '../../api/urls';
import { get } from '../../api/utils';
import deXSS from '../../utils/html/deXSS';
import noop from '../../utils/timing/noop';

interface ApiBanners {
system?: string;
configured?: string;
}

const allowedCssColorRegex = [
/^#(0x)?[0-9a-f]+$/i,
/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/
];
const allowedCssSizeRegex = [/^[\d.]+(?:px|pt|em|%|rem|vw)$/i];

// Based on defaults from https://github.com/apostrophecms/sanitize-html with support for a select set of styles that
// would make sense to use in a banner.
const sanitizeOptions: IOptions = {
allowedAttributes: {
'*': ['style'],
...sanitizeHtml.defaults.allowedAttributes
},
allowedStyles: {
'*': {
background: allowedCssColorRegex,
'background-color': allowedCssColorRegex,
color: allowedCssColorRegex,
direction: [/^ltr|rtl$/i],
'font-size': allowedCssSizeRegex,
height: allowedCssSizeRegex,
padding: allowedCssSizeRegex,
'padding-bottom': allowedCssSizeRegex,
'padding-left': allowedCssSizeRegex,
'padding-right': allowedCssSizeRegex,
'padding-top': allowedCssSizeRegex,
'text-align': [/^left|right|center$/i],
width: allowedCssSizeRegex
}
}
};

export const AppBanner = (): JSX.Element => {
const [banners, setBanners] = useState<ApiBanners>();

useEffect(() => {
if (!banners) {
get<ApiBanners>(BANNERS_API).then(setBanners).catch(noop);
}
});

return (
banners &&
(banners.system ? (
<div
className={'app-banner app-banner--system'}
dangerouslySetInnerHTML={{ __html: banners.system }}
/>
) : (
<div
className={'app-banner'}
dangerouslySetInnerHTML={{ __html: deXSS(banners.configured, sanitizeOptions) }}
/>
))
);
};

export default AppBanner;
3 changes: 3 additions & 0 deletions desktop/core/src/desktop/js/reactComponents/imports.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ export async function loadComponent(name) {
return (await import('../apps/editor/components/result/reactExample/ReactExample')).default;

// Application global components here
case 'AppBanner':
return (await import('./AppBanner/AppBanner')).default;

case 'ReactExampleGlobal':
return (await import('./ReactExampleGlobal/ReactExampleGlobal')).default;

Expand Down
6 changes: 3 additions & 3 deletions desktop/core/src/desktop/js/utils/html/deXSS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import sanitizeHtml from 'sanitize-html';
import sanitizeHtml, { IOptions } from 'sanitize-html';

const deXSS = (str?: boolean | string | number | null): string => {
const deXSS = (str?: undefined | boolean | string | number | null, options?: IOptions): string => {
if (str === null) {
return 'null';
}
if (typeof str !== 'undefined') {
return sanitizeHtml(str as string) || '';
return sanitizeHtml(str as string, options) || '';
}
return '';
};
Expand Down
2 changes: 1 addition & 1 deletion desktop/core/src/desktop/static/desktop/css/login.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit eb8b264

Please sign in to comment.