Merge pull request #2965 from balena-io/revamp-settings

Refactor settings page into modal
This commit is contained in:
Lorenzo Alberto Maria Ambrosi 2019-12-03 11:06:43 +01:00 committed by GitHub
commit 0ab967b7a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 853 additions and 524 deletions

2
.gitattributes vendored
View File

@ -1,6 +1,8 @@
# Javascript files must retain LF line-endings (to keep eslint happy)
*.js text eol=lf
*.jsx text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
# CSS and SCSS files must retain LF line-endings (to keep ensure-staged-sass.sh happy)
*.css text eol=lf
*.scss text eol=lf

View File

@ -94,7 +94,7 @@ const app = angular.module('Etcher', [
// Pages
require('./pages/main/main'),
require('./pages/finish/finish'),
require('./pages/settings/settings'),
require('./components/settings/index.ts').MODULE_NAME,
// OS
require('./os/open-external/open-external'),

View File

@ -189,7 +189,7 @@ const File = styled(UnstyledFile)`
display: flex;
justify-content: center;
text-align: center;
font-size: 14px;
font-size: 16px;
}
> div:last-child {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2016 resin.io
* Copyright 2019 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,14 +14,15 @@
* limitations under the License.
*/
.page-settings .checkbox input[type="checkbox"] + * {
color: $palette-theme-dark-foreground;
}
/**
* @module Etcher.Components.FeaturedProject
*/
.page-settings .checkbox input[type="checkbox"]:not(:checked) + * {
color: $palette-theme-dark-soft-foreground;
}
import * as angular from 'angular';
import { react2angular } from 'react2angular';
import { SettingsButton } from './settings';
.page-settings .title {
color: $palette-theme-dark-foreground;
}
export const MODULE_NAME = 'Etcher.Components.Settings';
const Settings = angular.module(MODULE_NAME, []);
Settings.component('settings', react2angular(SettingsButton));

View File

@ -0,0 +1,233 @@
/*
* Copyright 2019 balena.io
*
* Licensed 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 { faCog } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as _ from 'lodash';
import * as os from 'os';
import * as propTypes from 'prop-types';
import * as React from 'react';
import { Badge, Button, Checkbox, Modal, Provider } from 'rendition';
import styled from 'styled-components';
import * as settings from '../../models/settings';
import * as store from '../../models/store';
import * as analytics from '../../modules/analytics';
import { colors } from '../../theme';
const { useState } = React;
const platform = os.platform();
export const SettingsButton = () => {
const [hideModal, setHideModal] = useState(true);
return (
<Provider>
<Button
icon={<FontAwesomeIcon icon={faCog} />}
color={colors.secondary.background}
fontSize={24}
plain
onClick={() => setHideModal(false)}
tabIndex={5}
></Button>
{hideModal ? null : (
<SettingsModal toggleModal={(value: boolean) => setHideModal(!value)} />
)}
</Provider>
);
};
SettingsButton.propTypes = {};
interface WarningModalProps {
message: string;
confirmLabel: string;
cancel: () => void;
done: () => void;
}
const WarningModal = ({
message,
confirmLabel,
cancel,
done,
}: WarningModalProps) => {
return (
<Modal
title={confirmLabel}
action={confirmLabel}
cancel={cancel}
done={done}
style={{
width: 420,
height: 300,
}}
primaryButtonProps={{ warning: true }}
>
{message}
</Modal>
);
};
interface Setting {
name: string;
label: string | JSX.Element;
options?: any;
hide?: boolean;
}
const settingsList: Setting[] = [
{
name: 'errorReporting',
label: 'Anonymously report errors and usage statistics to balena.io',
},
{
name: 'unmountOnSuccess',
/**
* On Windows, "Unmounting" basically means "ejecting".
* On top of that, Windows users are usually not even
* familiar with the meaning of "unmount", which comes
* from the UNIX world.
*/
label: `${platform === 'win32' ? 'Eject' : 'Auto-unmount'} on success`,
},
{
name: 'validateWriteOnSuccess',
label: 'Validate write on success',
},
{
name: 'trim',
label: 'Trim ext{2,3,4} partitions before writing (raw images only)',
},
{
name: 'updatesEnabled',
label: 'Auto-updates enabled',
},
{
name: 'unsafeMode',
label: (
<span>
Unsafe mode{' '}
<Badge danger fontSize={12}>
Dangerous
</Badge>
</span>
),
options: {
description: `Are you sure you want to turn this on?
You will be able to overwrite your system drives if you're not careful.`,
confirmLabel: 'Enable unsafe mode',
},
hide: settings.get('disableUnsafeMode'),
},
];
interface SettingsModalProps {
toggleModal: (value: boolean) => void;
}
export const SettingsModal: any = styled(
({ toggleModal }: SettingsModalProps) => {
const [currentSettings, setCurrentSettings] = useState(settings.getAll());
const [warning, setWarning]: [
any,
React.Dispatch<React.SetStateAction<any>>,
] = useState({});
const toggleSetting = async (setting: string, options?: any) => {
const value = currentSettings[setting];
const dangerous = !_.isUndefined(options);
analytics.logEvent('Toggle setting', {
setting,
value,
dangerous,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
});
if (value || !dangerous) {
await settings.set(setting, !value);
setCurrentSettings({
...currentSettings,
[setting]: !value,
});
setWarning({});
return;
}
// Show warning since it's a dangerous setting
setWarning({
setting,
settingValue: value,
...options,
});
};
return (
<Modal
id="settings-modal"
title="Settings"
done={() => toggleModal(false)}
style={{
width: 780,
height: 420,
}}
>
<div>
{_.map(settingsList, (setting: Setting, i: number) => {
return setting.hide ? null : (
<div key={setting.name}>
<Checkbox
toggle
tabIndex={6 + i}
label={setting.label}
checked={currentSettings[setting.name]}
onChange={() => toggleSetting(setting.name, setting.options)}
/>
</div>
);
})}
</div>
{_.isEmpty(warning) ? null : (
<WarningModal
message={warning.description}
confirmLabel={warning.confirmLabel}
done={() => {
settings.set(warning.setting, !warning.settingValue);
setCurrentSettings({
...currentSettings,
[warning.setting]: true,
});
setWarning({});
}}
cancel={() => {
setWarning({});
}}
/>
)}
</Modal>
);
},
)`
> div:nth-child(3) {
justify-content: center;
}
`;
SettingsModal.propTypes = {
toggleModal: propTypes.func,
};

View File

@ -11,57 +11,27 @@
</head>
<body>
<header class="section-header" ng-controller="HeaderController as header">
<button class="button button-link"
ng-if="header.shouldShowHelp()"
ng-click="header.openHelpPage()"
tabindex="4">
<span class="glyphicon glyphicon-question-sign"></span>
</button>
<button class="button button-link"
ui-sref="settings"
hide-if-state="settings"
tabindex="5">
<span class="glyphicon glyphicon-cog"></span>
</button>
<button class="button button-link"
tabindex="5"
ui-sref="main"
show-if-state="settings">
<span class="glyphicon glyphicon-chevron-left"></span> Back
</button>
</header>
<main class="wrapper" ui-view></main>
<footer class="section-footer-main" ng-controller="StateController as state"
ng-hide="state.currentName === 'success'">
<span os-open-external="https://www.balena.io/etcher?ref=etcher_footer"
<span
id="app-logo"
os-open-external="https://www.balena.io/etcher?ref=etcher_footer"
tabindex="100">
<svg-icon paths="[ '../../assets/etcher.svg' ]"
width="'123px'"
height="'22px'"></svg-icon>
</span>
<span class="caption">
is <span class="caption"
tabindex="101"
os-open-external="https://github.com/balena-io/etcher">an open source project</span> by
</span>
<settings tabindex="4">
</settings>
<span os-open-external="https://www.balena.io?ref=etcher"
tabindex="102">
<svg-icon paths="[ '../../assets/balena.svg' ]"
width="'79px'"
height="'23px'"></svg-icon>
</span>
<button class="button button-link"
ng-if="header.shouldShowHelp()"
ng-click="header.openHelpPage()"
tabindex="5">
<span class="glyphicon glyphicon-question-sign"></span>
</button>
</header>
<span class="caption footer-right"
tabindex="103"
manifest-bind="version"
os-open-external="https://github.com/balena-io/etcher/blob/master/CHANGELOG.md"></span>
</footer>
<main class="wrapper" ui-view></main>
<div class="section-loader"
ng-controller="StateController as state"

View File

@ -120,7 +120,7 @@ svg-icon > img[disabled] {
}
.page-main .button.step-footer {
font-size: 14px;
font-size: 16px;
color: $palette-theme-primary-background;
border-radius: 0;
padding: 0;
@ -166,7 +166,7 @@ svg-icon > img[disabled] {
.page-main .step-size {
color: $palette-theme-dark-disabled-foreground;
margin: 0 0 8px 0;
font-size: 14px;
font-size: 16px;
line-height: 1.5;
height: 21px;
width: 100%;
@ -191,7 +191,7 @@ svg-icon > img[disabled] {
.target-status-line {
display: flex;
align-items: baseline;
font-size: 14px;
font-size: 16px;
font-family: inherit;
> .target-status-dot {
@ -226,3 +226,17 @@ svg-icon > img[disabled] {
.space-vertical-large {
position: relative;
}
body.rendition-modal-open > div:last-child > div > div > div:last-child {
top: unset;
bottom: -200px;
}
#app-logo {
position: fixed;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
width: 123px;
}

View File

@ -1,123 +0,0 @@
/*
* Copyright 2016 resin.io
*
* Licensed 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.
*/
'use strict'
const os = require('os')
const _ = require('lodash')
const store = require('../../../models/store')
const settings = require('../../../models/settings')
const analytics = require('../../../modules/analytics')
const exceptionReporter = require('../../../modules/exception-reporter')
module.exports = function (WarningModalService) {
/**
* @summary Client platform
* @type {String}
* @constant
* @public
*/
this.platform = os.platform()
/**
* @summary Refresh current settings
* @function
* @public
*
* @example
* SettingsController.refreshSettings();
*/
this.refreshSettings = () => {
this.currentData = settings.getAll()
}
/**
* @summary Current settings value
* @type {Object}
* @public
*/
this.currentData = {}
this.refreshSettings()
/**
* @summary Settings model
* @type {Object}
* @public
*/
this.model = settings
/**
* @summary Toggle setting
* @function
* @public
*
* @description
* If warningOptions is given, it should be an object having `description` and `confirmationLabel`;
* these will be used to present a user confirmation modal before enabling the setting.
* If warningOptions is missing, no confirmation modal is displayed.
*
* @param {String} setting - setting key
* @param {Object} [options] - options
* @param {String} [options.description] - warning modal description
* @param {String} [options.confirmationLabel] - warning modal confirmation label
* @returns {Undefined}
*
* @example
* SettingsController.toggle('unsafeMode', {
* description: 'Don\'t do this!',
* confirmationLabel: 'Do it!'
* });
*/
this.toggle = (setting, options) => {
const value = this.currentData[setting]
const dangerous = !_.isUndefined(options)
analytics.logEvent('Toggle setting', {
setting,
value,
dangerous,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid
})
if (!value || !dangerous) {
return this.model.set(setting, value)
}
// Keep the checkbox unchecked until the user confirms
this.currentData[setting] = false
return WarningModalService.display(options).then((userAccepted) => {
if (userAccepted) {
this.model.set(setting, true)
this.refreshSettings()
}
}).catch(exceptionReporter.report)
}
/**
* @summary Show unsafe mode based on an env var
* @function
* @public
*
* @returns {Boolean}
*
* @example
* SettingsController.shouldShowUnsafeMode()
*/
this.shouldShowUnsafeMode = () => {
return !settings.get('disableUnsafeMode')
}
}

View File

@ -1,41 +0,0 @@
/*
* Copyright 2016 resin.io
*
* Licensed 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.
*/
'use strict'
/**
* @module Etcher.Pages.Settings
*/
const angular = require('angular')
const MODULE_NAME = 'Etcher.Pages.Settings'
const SettingsPage = angular.module(MODULE_NAME, [
require('angular-ui-router'),
require('../../components/warning-modal/warning-modal')
])
SettingsPage.controller('SettingsController', require('./controllers/settings'))
SettingsPage.config(($stateProvider) => {
$stateProvider
.state('settings', {
url: '/settings',
controller: 'SettingsController as settings',
template: require('./templates/settings.tpl.html')
})
})
module.exports = MODULE_NAME

View File

@ -1,80 +0,0 @@
<div class="page-settings text-left">
<h1 class="title space-bottom-large">Settings</h1>
<div class="checkbox">
<label>
<input type="checkbox"
tabindex="6"
ng-model="settings.currentData.errorReporting"
ng-change="settings.toggle('errorReporting')">
<span>Anonymously report errors and usage statistics to balena.io</span>
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox"
tabindex="7"
ng-model="settings.currentData.unmountOnSuccess"
ng-change="settings.toggle('unmountOnSuccess')">
<!-- On Windows, "Unmounting" basically means "ejecting". -->
<!-- On top of that, Windows users are usually not even -->
<!-- familiar with the meaning of "unmount", which comes -->
<!-- from the UNIX world. -->
<span>
<span ng-show="settings.platform == 'win32'">Eject</span>
<span ng-hide="settings.platform == 'win32'">Auto-unmount</span>
on success
</span>
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox"
tabindex="8"
ng-model="settings.currentData.validateWriteOnSuccess"
ng-change="settings.toggle('validateWriteOnSuccess')">
<span>Validate write on success</span>
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox"
tabindex="8"
ng-model="settings.currentData.trim"
ng-change="settings.toggle('trim')">
<span>Trim ext{2,3,4} partitions before writing (raw images only)</span>
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox"
tabindex="9"
ng-model="settings.currentData.updatesEnabled"
ng-change="settings.toggle('updatesEnabled')">
<span>Auto-updates enabled</span>
</label>
</div>
<div class="checkbox" ng-if="settings.shouldShowUnsafeMode()">
<label>
<input type="checkbox"
tabindex="10"
ng-model="settings.currentData.unsafeMode"
ng-change="settings.toggle('unsafeMode', {
description: 'Are you sure you want to turn this on? You will be able to overwrite your system drives if you\'re not careful.',
confirmationLabel: 'Enable unsafe mode'
})">
<span>Unsafe mode <span class="label label-danger">Dangerous</span></span>
</label>
</div>
</div>

View File

@ -29,8 +29,9 @@
position: relative;
> .glyphicon {
top: 2px;
margin-right: 2px;
top: 0;
width: 24px;
height: 24px;
}
&.button-primary{

View File

@ -38,7 +38,6 @@ $disabled-opacity: 0.2;
@import "../components/warning-modal/styles/warning-modal";
@import "../components/file-selector/styles/file-selector";
@import "../pages/main/styles/main";
@import "../pages/settings/styles/settings";
@import "../pages/finish/styles/finish";
$fa-font-path: "../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts";
@ -212,11 +211,21 @@ body {
.section-header {
text-align: right;
padding: 5px 8px;
padding: 13px 14px;
> .button {
padding-left: 3px;
padding-right: 3px;
padding: 0;
> .glyphicon {
font-size: 24px;
}
}
> * {
display: inline-block;
vertical-align: middle;
height: 24px;
margin: 0 10px;
}
}

View File

@ -48,6 +48,10 @@ exports.colors = {
foreground: '#fff',
background: '#2297de'
},
secondary: {
foreground: '#000',
background: '#ddd'
},
warning: {
foreground: '#fff',
background: '#fca321'

View File

@ -6035,8 +6035,9 @@ body {
outline: none;
position: relative; }
.button > .glyphicon, .button > .tick {
top: 2px;
margin-right: 2px; }
top: 0;
width: 24px;
height: 24px; }
.button.button-primary {
width: 200px;
height: 48px; }
@ -6452,7 +6453,7 @@ svg-icon > img[disabled] {
padding-bottom: 2px; }
.page-main .button.step-footer {
font-size: 14px;
font-size: 16px;
color: #2297de;
border-radius: 0;
padding: 0;
@ -6489,7 +6490,7 @@ svg-icon > img[disabled] {
.page-main .step-size {
color: #787c7f;
margin: 0 0 8px 0;
font-size: 14px;
font-size: 16px;
line-height: 1.5;
height: 21px;
width: 100%; }
@ -6511,7 +6512,7 @@ svg-icon > img[disabled] {
.target-status-line {
display: flex;
align-items: baseline;
font-size: 14px;
font-size: 16px;
font-family: inherit; }
.target-status-line > .target-status-dot {
width: 12px;
@ -6535,29 +6536,17 @@ svg-icon > img[disabled] {
.space-vertical-large {
position: relative; }
/*
* Copyright 2016 resin.io
*
* Licensed 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.
*/
.page-settings .checkbox input[type="checkbox"] + * {
color: #fff; }
body.rendition-modal-open > div:last-child > div > div > div:last-child {
top: unset;
bottom: -200px; }
.page-settings .checkbox input[type="checkbox"]:not(:checked) + * {
color: #ddd; }
.page-settings .title {
color: #fff; }
#app-logo {
position: fixed;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
width: 123px; }
/*
* Copyright 2016 resin.io
@ -9973,10 +9962,16 @@ body {
.section-header {
text-align: right;
padding: 5px 8px; }
padding: 13px 14px; }
.section-header > .button {
padding-left: 3px;
padding-right: 3px; }
padding: 0; }
.section-header > .button > .glyphicon, .section-header > .button > .tick {
font-size: 24px; }
.section-header > * {
display: inline-block;
vertical-align: middle;
height: 24px;
margin: 0 10px; }
featured-project webview {
flex: 0 1;

659
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -40,6 +40,9 @@
],
"dependencies": {
"@fortawesome/fontawesome-free-webfonts": "^1.0.9",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/react-fontawesome": "^0.1.7",
"angular": "1.7.6",
"angular-if-state": "^1.0.0",
"angular-moment": "^1.0.1",
@ -68,7 +71,7 @@
"react-dom": "^16.8.5",
"react2angular": "^4.0.2",
"redux": "^3.5.2",
"rendition": "^8.7.2",
"rendition": "^11.24.0",
"request": "^2.81.0",
"resin-corvus": "^2.0.3",
"roboto-fontface": "^0.9.0",

View File

@ -25,6 +25,9 @@ angularValidate.validate(
path.join(PROJECT_ROOT, 'lib', 'gui', '**/*.html')
],
{
customtags: [
'settings'
],
customattrs: [
// Internal

View File

@ -1,46 +0,0 @@
/*
* Copyright 2018 resin.io
*
* Licensed 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.
*/
'use strict'
const m = require('mochainon')
const fs = require('fs')
const angular = require('angular')
require('angular-mocks')
describe('Browser: SettingsPage', function () {
beforeEach(angular.mock.module(
require('../../../lib/gui/app/pages/settings/settings')
))
describe('page template', function () {
let $state
beforeEach(angular.mock.inject(function (_$state_) {
$state = _$state_
}))
it('should match the file contents', function () {
const {
template
} = $state.get('settings')
const contents = fs.readFileSync('lib/gui/app/pages/settings/templates/settings.tpl.html', {
encoding: 'utf-8'
})
m.chai.expect(template).to.equal(contents)
})
})
})

View File

@ -9,7 +9,8 @@
"moduleResolution": "node",
"module": "commonjs",
"target": "es2017",
"jsx": "react"
"jsx": "react",
"allowSyntheticDefaultImports": true
},
"include": [
"lib/**/*.ts",