From c6a3528edf48b9f05a4532be7f109ed5e24d5e0a Mon Sep 17 00:00:00 2001 From: maggieneterval Date: Wed, 30 Jan 2019 10:07:20 -0500 Subject: [PATCH] feat(core): add application-specific custom banners --- .../config/applicationConfig.controller.js | 28 ++- .../config/applicationConfig.view.html | 9 + .../customBanner/CustomBannerConfig.spec.tsx | 83 +++++++ .../customBanner/CustomBannerConfig.tsx | 230 ++++++++++++++++++ .../config/customBanner/customBannerColors.ts | 71 ++++++ .../customBannerConfig.component.ts | 10 + .../customBanner/customBannerConfig.less | 27 ++ .../core/src/bootstrap/bootstrap.module.ts | 7 +- .../core/src/bootstrap/customBanner.html | 1 + .../core/src/bootstrap/spinnaker.component.ts | 1 + app/scripts/modules/core/src/core.module.ts | 2 + .../src/header/customBanner/CustomBanner.less | 9 + .../header/customBanner/CustomBanner.spec.tsx | 47 ++++ .../src/header/customBanner/CustomBanner.tsx | 77 ++++++ .../customBanner/customBanner.component.ts | 7 + 15 files changed, 607 insertions(+), 2 deletions(-) create mode 100644 app/scripts/modules/core/src/application/config/customBanner/CustomBannerConfig.spec.tsx create mode 100644 app/scripts/modules/core/src/application/config/customBanner/CustomBannerConfig.tsx create mode 100644 app/scripts/modules/core/src/application/config/customBanner/customBannerColors.ts create mode 100644 app/scripts/modules/core/src/application/config/customBanner/customBannerConfig.component.ts create mode 100755 app/scripts/modules/core/src/application/config/customBanner/customBannerConfig.less create mode 100644 app/scripts/modules/core/src/bootstrap/customBanner.html create mode 100644 app/scripts/modules/core/src/header/customBanner/CustomBanner.less create mode 100644 app/scripts/modules/core/src/header/customBanner/CustomBanner.spec.tsx create mode 100644 app/scripts/modules/core/src/header/customBanner/CustomBanner.tsx create mode 100644 app/scripts/modules/core/src/header/customBanner/customBanner.component.ts diff --git a/app/scripts/modules/core/src/application/config/applicationConfig.controller.js b/app/scripts/modules/core/src/application/config/applicationConfig.controller.js index 1817b8266b4..10121193d81 100644 --- a/app/scripts/modules/core/src/application/config/applicationConfig.controller.js +++ b/app/scripts/modules/core/src/application/config/applicationConfig.controller.js @@ -1,7 +1,10 @@ +import { cloneDeep } from 'lodash'; + import { APPLICATION_DATA_SOURCE_EDITOR } from './dataSources/applicationDataSourceEditor.component'; import { CHAOS_MONKEY_CONFIG_COMPONENT } from 'core/chaosMonkey/chaosMonkeyConfig.component'; import { TRAFFIC_GUARD_CONFIG_COMPONENT } from './trafficGuard/trafficGuardConfig.component'; import { SETTINGS } from 'core/config/settings'; +import { ApplicationWriter } from 'core/application/service/ApplicationWriter'; const angular = require('angular'); @@ -18,7 +21,7 @@ module.exports = angular TRAFFIC_GUARD_CONFIG_COMPONENT, require('./links/applicationLinks.component').name, ]) - .controller('ApplicationConfigController', function($state, app) { + .controller('ApplicationConfigController', function($state, app, $scope) { this.application = app; this.isDataSourceEnabled = key => app.dataSources.some(ds => ds.key === key && ds.disabled === false); this.feature = SETTINGS.feature; @@ -28,4 +31,27 @@ module.exports = angular this.application.attributes.instancePort = this.application.attributes.instancePort || SETTINGS.defaultInstancePort || null; } + this.bannerConfigProps = { + isSaving: false, + saveError: false, + }; + this.updateBannerConfigs = bannerConfigs => { + const applicationAttributes = cloneDeep(this.application.attributes); + applicationAttributes.customBanners = bannerConfigs; + $scope.$applyAsync(() => { + this.bannerConfigProps.isSaving = true; + this.bannerConfigProps.saveError = false; + }); + ApplicationWriter.updateApplication(applicationAttributes) + .then(() => { + $scope.$applyAsync(() => { + this.bannerConfigProps.isSaving = false; + this.application.attributes = applicationAttributes; + }); + }) + .catch(() => { + this.bannerConfigProps.isSaving = false; + this.bannerConfigProps.saveError = true; + }); + }; }); diff --git a/app/scripts/modules/core/src/application/config/applicationConfig.view.html b/app/scripts/modules/core/src/application/config/applicationConfig.view.html index 41bb2e435f7..9b494fbcc16 100644 --- a/app/scripts/modules/core/src/application/config/applicationConfig.view.html +++ b/app/scripts/modules/core/src/application/config/applicationConfig.view.html @@ -24,6 +24,15 @@ + + + + diff --git a/app/scripts/modules/core/src/application/config/customBanner/CustomBannerConfig.spec.tsx b/app/scripts/modules/core/src/application/config/customBanner/CustomBannerConfig.spec.tsx new file mode 100644 index 00000000000..72f8b3008f3 --- /dev/null +++ b/app/scripts/modules/core/src/application/config/customBanner/CustomBannerConfig.spec.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; + +import { noop } from 'core/utils'; + +import { CustomBannerConfig, ICustomBannerConfig } from './CustomBannerConfig'; + +describe('', () => { + let bannerConfigs: ICustomBannerConfig[]; + let wrapper: any; + + beforeEach(() => { + bannerConfigs = getTestBannerConfigs(); + wrapper = shallow( + , + ); + }); + + describe('view', () => { + it('renders a row for each banner config', () => { + expect(wrapper.find('.custom-banner-config-row').length).toEqual(bannerConfigs.length); + }); + it('renders an "add" button', () => { + expect(wrapper.find('.add-new').length).toEqual(1); + }); + }); + + describe('functionality', () => { + it('update banner config', () => { + expect(wrapper.state('bannerConfigsEditing')).toEqual(bannerConfigs); + wrapper + .find('input[type="checkbox"]') + .at(1) + .simulate('change', { target: { checked: true } }); + const updatedConfigs = [ + { + ...bannerConfigs[0], + enabled: false, + }, + { + ...bannerConfigs[1], + enabled: true, + }, + ]; + expect(wrapper.state('bannerConfigsEditing')).toEqual(updatedConfigs); + }); + it('add banner config', () => { + expect(wrapper.state('bannerConfigsEditing').length).toEqual(2); + wrapper.find('.add-new').simulate('click'); + expect(wrapper.state('bannerConfigsEditing').length).toEqual(3); + }); + it('remove banner config', () => { + expect(wrapper.state('bannerConfigsEditing').length).toEqual(2); + wrapper + .find('.custom-banner-config-remove') + .at(1) + .simulate('click'); + expect(wrapper.state('bannerConfigsEditing').length).toEqual(1); + }); + }); +}); + +export function getTestBannerConfigs(): ICustomBannerConfig[] { + return [ + { + backgroundColor: 'var(--color-alert)', + enabled: true, + text: 'Warning: currently in maintenance mode', + textColor: 'var(--color-text-on-dark)', + }, + { + backgroundColor: 'var(--color-alert)', + enabled: false, + text: 'Warning: currently in production freeze', + textColor: 'var(--color-text-on-dark)', + }, + ]; +} diff --git a/app/scripts/modules/core/src/application/config/customBanner/CustomBannerConfig.tsx b/app/scripts/modules/core/src/application/config/customBanner/CustomBannerConfig.tsx new file mode 100644 index 00000000000..120add910d3 --- /dev/null +++ b/app/scripts/modules/core/src/application/config/customBanner/CustomBannerConfig.tsx @@ -0,0 +1,230 @@ +import * as React from 'react'; +import { isEqual } from 'lodash'; +import Select, { Option } from 'react-select'; + +import { ConfigSectionFooter } from 'core/application/config/footer/ConfigSectionFooter'; +import { + bannerBackgroundColorOptions, + bannerTextColorOptions, +} from 'core/application/config/customBanner/customBannerColors'; + +import { noop } from 'core/utils'; + +import './customBannerConfig.less'; + +export interface ICustomBannerConfig { + backgroundColor: string; + enabled: boolean; + text: string; + textColor: string; +} + +export interface ICustomBannerConfigProps { + bannerConfigs: ICustomBannerConfig[]; + isSaving: boolean; + saveError: boolean; + updateBannerConfigs: (bannerConfigs: ICustomBannerConfig[]) => void; +} + +export interface ICustomBannerConfigState { + bannerConfigsEditing: ICustomBannerConfig[]; +} + +export class CustomBannerConfig extends React.Component { + public static defaultProps: Partial = { + bannerConfigs: [], + isSaving: false, + saveError: false, + updateBannerConfigs: noop, + }; + + constructor(props: ICustomBannerConfigProps) { + super(props); + this.state = { + bannerConfigsEditing: props.bannerConfigs, + }; + } + + private onEnabledChange = (idx: number, isChecked: boolean) => { + this.setState({ + bannerConfigsEditing: this.state.bannerConfigsEditing.map((config, i) => { + if (i === idx) { + return { + ...config, + enabled: isChecked, + }; + } + // Only one config can be enabled + return { + ...config, + enabled: isChecked ? false : config.enabled, + }; + }), + }); + }; + + private onTextChange = (idx: number, text: string) => { + this.setState({ + bannerConfigsEditing: this.state.bannerConfigsEditing.map((config, i) => { + if (i === idx) { + return { + ...config, + text, + }; + } + return config; + }), + }); + }; + + private onTextColorChange = (idx: number, option: Option) => { + this.setState({ + bannerConfigsEditing: this.state.bannerConfigsEditing.map((config, i) => { + if (i === idx) { + return { + ...config, + textColor: option.value, + }; + } + return config; + }), + }); + }; + + private onBackgroundColorChange = (idx: number, option: Option) => { + this.setState({ + bannerConfigsEditing: this.state.bannerConfigsEditing.map((config, i) => { + if (i === idx) { + return { + ...config, + backgroundColor: option.value, + }; + } + return config; + }), + }); + }; + + private addBanner = (): void => { + this.setState({ + bannerConfigsEditing: this.state.bannerConfigsEditing.concat([ + { + backgroundColor: 'var(--color-alert)', + enabled: false, + text: 'Your custom banner text', + textColor: 'var(--color-text-on-dark)', + } as ICustomBannerConfig, + ]), + }); + }; + + private removeBanner = (idx: number): void => { + this.setState({ + bannerConfigsEditing: this.state.bannerConfigsEditing.filter((_config, i) => i !== idx), + }); + }; + + private isDirty = (): boolean => { + return !isEqual(this.props.bannerConfigs, this.state.bannerConfigsEditing); + }; + + private onRevertClicked = (): void => { + this.setState({ + bannerConfigsEditing: this.props.bannerConfigs, + }); + }; + + private onSaveClicked = (): void => { + this.props.updateBannerConfigs(this.state.bannerConfigsEditing); + }; + + private colorOptionRenderer = (option: Option): JSX.Element => { + return
; + }; + + public render() { + return ( +
+
+ Custom Banners allow you to specify application-specific headers that will appear above the main Spinnaker + navigation bar. +
+
+ + + + + + + + + + + {this.state.bannerConfigsEditing.map((banner, idx) => ( + + +
EnabledTextText ColorBackground +
+ ) => this.onEnabledChange(idx, e.target.checked)} + type="checkbox" + /> + +