Add UI/UX polish in prep for merge

* Bring UI/UX inline with recent changes
* Use select components as a stopgap for credential_types and orgs
* Add tabs to permissions view
* Add Organization model
This commit is contained in:
gconsidine
2017-06-16 15:23:18 -04:00
parent 861cfd3e13
commit fa330db9c5
26 changed files with 145 additions and 43 deletions

View File

@@ -1,11 +1,14 @@
const DEFAULT_ORGANIZATION_PLACEHOLDER = 'SELECT AN ORGANIZATION';
function AddCredentialsController (models, $state) { function AddCredentialsController (models, $state) {
let vm = this || {}; let vm = this || {};
let me = models.me; let me = models.me;
let credential = models.credential; let credential = models.credential;
let credentialType = models.credentialType; let credentialType = models.credentialType;
let organization = models.organization;
vm.panelTitle = 'New Credential'; vm.panelTitle = 'NEW CREDENTIAL';
vm.tab = { vm.tab = {
details: { details: {
@@ -20,6 +23,13 @@ function AddCredentialsController (models, $state) {
omit: ['user', 'team', 'inputs'] omit: ['user', 'team', 'inputs']
}); });
vm.form.organization._placeholder = DEFAULT_ORGANIZATION_PLACEHOLDER;
vm.form.organization._data = organization.get('results');
vm.form.organization._format = 'objects';
vm.form.organization._exp = 'org as org.name for org in state._data';
vm.form.organization._display = 'name';
vm.form.organization._key = 'id';
vm.form.credential_type._data = credentialType.get('results'); vm.form.credential_type._data = credentialType.get('results');
vm.form.credential_type._placeholder = 'SELECT A TYPE'; vm.form.credential_type._placeholder = 'SELECT A TYPE';
vm.form.credential_type._format = 'grouped-object'; vm.form.credential_type._format = 'grouped-object';

View File

@@ -10,7 +10,7 @@
<at-form state="vm.form"> <at-form state="vm.form">
<at-input-text col="4" tab="1" state="vm.form.name"></at-input-text> <at-input-text col="4" tab="1" state="vm.form.name"></at-input-text>
<at-input-text col="4" tab="2" state="vm.form.description"></at-input-text> <at-input-text col="4" tab="2" state="vm.form.description"></at-input-text>
<at-input-lookup col="4" tab="3" state="vm.form.organization"></at-input-lookup> <at-input-select col="4" tab="3" state="vm.form.organization"></at-input-select>
<at-divider></at-divider> <at-divider></at-divider>
@@ -32,6 +32,11 @@
$state.current.name === 'credentials.edit.permissions.add'"> $state.current.name === 'credentials.edit.permissions.add'">
<at-panel-heading>Credentials Permissions</at-panel-heading> <at-panel-heading>Credentials Permissions</at-panel-heading>
<at-tab-group>
<at-tab state="vm.tab.details">Details</at-tab>
<at-tab state="vm.tab.permissions">Permissions</at-tab>
</at-tab-group>
<at-panel-body> <at-panel-body>
<div class="at-CredentialsPermissions" ui-view="related"></div> <div class="at-CredentialsPermissions" ui-view="related"></div>
</at-panel-body> </at-panel-body>

View File

@@ -1,13 +1,18 @@
const DEFAULT_ORGANIZATION_PLACEHOLDER = 'SELECT AN ORGANIZATION';
function EditCredentialsController (models, $state, $scope) { function EditCredentialsController (models, $state, $scope) {
let vm = this || {}; let vm = this || {};
let me = models.me; let me = models.me;
let credential = models.credential; let credential = models.credential;
let credentialType = models.credentialType; let credentialType = models.credentialType;
let organization = models.organization;
vm.tab = { vm.tab = {
details: { details: {
_active: true _active: true,
_go: 'credentials.edit',
_params: { credential_id: credential.get('id') }
}, },
permissions:{ permissions:{
_go: 'credentials.edit.permissions', _go: 'credentials.edit.permissions',
@@ -15,6 +20,16 @@ function EditCredentialsController (models, $state, $scope) {
} }
}; };
$scope.$watch('$state.current.name', (value) => {
if (value === 'credentials.edit') {
vm.tab.details._active = true;
vm.tab.details._permissions = false;
} else {
vm.tab.permissions._active = true;
vm.tab.details._active = false;
}
});
// Only exists for permissions compatibility // Only exists for permissions compatibility
$scope.credential_obj = credential.get(); $scope.credential_obj = credential.get();
@@ -24,6 +39,14 @@ function EditCredentialsController (models, $state, $scope) {
omit: ['user', 'team', 'inputs'] omit: ['user', 'team', 'inputs']
}); });
vm.form.organization._placeholder = DEFAULT_ORGANIZATION_PLACEHOLDER;
vm.form.organization._data = organization.get('results');
vm.form.organization._format = 'objects';
vm.form.organization._exp = 'org as org.name for org in state._data';
vm.form.organization._display = 'name';
vm.form.organization._key = 'id';
vm.form.organization._value = organization.getById(credential.get('organization'));
vm.form.credential_type._data = credentialType.get('results'); vm.form.credential_type._data = credentialType.get('results');
vm.form.credential_type._format = 'grouped-object'; vm.form.credential_type._format = 'grouped-object';
vm.form.credential_type._display = 'name'; vm.form.credential_type._display = 'name';

View File

@@ -6,12 +6,13 @@ import AddController from './add-credentials.controller.js';
import EditController from './edit-credentials.controller.js'; import EditController from './edit-credentials.controller.js';
import { N_ } from '../../src/i18n'; import { N_ } from '../../src/i18n';
function CredentialsResolve ($q, $stateParams, Me, Credential, CredentialType) { function CredentialsResolve ($q, $stateParams, Me, Credential, CredentialType, Organization) {
let id = $stateParams.credential_id; let id = $stateParams.credential_id;
let promises = { let promises = {
me: new Me('get'), me: new Me('get'),
credentialType: new CredentialType('get') credentialType: new CredentialType('get'),
organization: new Organization('get')
}; };
if (id) { if (id) {
@@ -28,13 +29,13 @@ CredentialsResolve.$inject = [
'$stateParams', '$stateParams',
'MeModel', 'MeModel',
'CredentialModel', 'CredentialModel',
'CredentialTypeModel' 'CredentialTypeModel',
'OrganizationModel'
]; ];
function CredentialsConfig ($stateProvider, $stateExtenderProvider, stateDefinitionsProvider, pathServiceProvider) { function CredentialsConfig ($stateProvider, $stateExtenderProvider, pathServiceProvider) {
let pathService = pathServiceProvider.$get(); let pathService = pathServiceProvider.$get();
let stateExtender = $stateExtenderProvider.$get(); let stateExtender = $stateExtenderProvider.$get();
let stateDefinitions = stateDefinitionsProvider.$get();
stateExtender.addState({ stateExtender.addState({
name: 'credentials', name: 'credentials',
@@ -272,7 +273,6 @@ function CredentialsConfig ($stateProvider, $stateExtenderProvider, stateDefinit
CredentialsConfig.$inject = [ CredentialsConfig.$inject = [
'$stateProvider', '$stateProvider',
'$stateExtenderProvider', '$stateExtenderProvider',
'stateDefinitionsProvider',
'PathServiceProvider' 'PathServiceProvider'
]; ];

View File

@@ -7,7 +7,7 @@
color: @at-gray-dark-5x; color: @at-gray-dark-5x;
&, &:active { &, &:active {
border-color: @at-gray-dark-3x; border-color: @at-gray-dark-2x;
} }
&:focus { &:focus {
@@ -19,25 +19,33 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
& > input[type=checkbox] { & > label {
height: @at-input-height; & > input[type=checkbox] {
margin: 0; height: @at-input-height;
padding: 0; margin: 0;
padding: 0;
}
& > p {
margin: 0;
padding: 0 0 0 @at-space-6x;
line-height: @at-line-height-tall;
}
} }
} }
.at-InputContainer { .at-InputContainer {
margin-top: @at-space-6x;; margin-top: @at-space-6x;
} }
.at-Input-button { .at-Input-button {
min-width: @at-input-button-width; min-width: @at-input-button-width;
display: block; display: block;
height: 100%; height: @at-input-height;
&, &:active, &:hover, &:focus { &, &:active, &:hover, &:focus {
color: @at-gray-dark-3x; color: @at-gray-dark-3x;
border-color: @at-gray-dark-3x; border-color: @at-gray-dark-2x;
background-color: @at-white; background-color: @at-white;
cursor: pointer; cursor: pointer;
} }
@@ -76,12 +84,16 @@
position: absolute; position: absolute;
width: @at-inset-width; width: @at-inset-width;
height: 100%; height: 100%;
background: @at-gray; background: @at-gray-dark;
left: -@at-inset-width; left: -@at-inset-width;
} }
.at-InputGroup-button { .at-InputGroup-button {
height: 100%; height: 100%;
& > button {
height: 100%;
}
} }
.at-InputGroup-title { .at-InputGroup-title {

View File

@@ -84,12 +84,14 @@ function BaseInputController () {
scope.state._enableToggle = true; scope.state._enableToggle = true;
scope.state._value = scope.state._preEditValue; scope.state._value = scope.state._preEditValue;
scope.state._activeModel = '_displayValue'; scope.state._activeModel = '_displayValue';
scope.state._placeholder = 'ENCRYPTED';
} else { } else {
scope.state._buttonText = 'REVERT'; scope.state._buttonText = 'REVERT';
scope.state._disabled = false; scope.state._disabled = false;
scope.state._enableToggle = false; scope.state._enableToggle = false;
scope.state._activeModel = '_value'; scope.state._activeModel = '_value';
scope.state._value = ''; scope.state._value = '';
scope.state._placeholder = '';
} }
}; };

View File

@@ -14,6 +14,8 @@ function AtInputCheckboxController (baseInputController) {
vm.init = (scope, element, form) => { vm.init = (scope, element, form) => {
baseInputController.call(vm, 'input', scope, element, form); baseInputController.call(vm, 'input', scope, element, form);
scope.label = scope.state.label;
scope.state.label = 'OPTIONS';
vm.check(); vm.check();
}; };

View File

@@ -2,12 +2,15 @@
<div class="form-group at-u-flat"> <div class="form-group at-u-flat">
<at-input-label></at-input-label> <at-input-label></at-input-label>
<div class="checkbox at-InputCheckbox"> <div class="checkbox at-InputCheckbox">
<input type="checkbox" <label>
ng-class="{ 'at-Input--rejected': state.rejected }" <input type="checkbox"
ng-model="state._value" ng-class="{ 'at-Input--rejected': state.rejected }"
ng-attr-tabindex="{{ tab || undefined }}" ng-model="state._value"
ng-change="vm.check()" ng-attr-tabindex="{{ tab || undefined }}"
ng-disabled="state._disabled || form.disabled" /> ng-change="vm.check()"
ng-disabled="state._disabled || form.disabled" />
<p>{{ label }}</p>
</label>
</div> </div>
<at-input-message></at-input-message> <at-input-message></at-input-message>
</div> </div>

View File

@@ -8,7 +8,7 @@
<input type="checkbox" <input type="checkbox"
ng-model="state._promptOnLaunch" ng-model="state._promptOnLaunch"
ng-change="vm.togglePromptOnLaunch()" /> ng-change="vm.togglePromptOnLaunch()" />
<p>Prompt on launch?</p> <p>Prompt on launch</p>
</label> </label>
</div> </div>
</label> </label>

View File

@@ -26,6 +26,7 @@ function AtInputSecretController (baseInputController) {
vm.toggle = vm.toggleShowHide; vm.toggle = vm.toggleShowHide;
} else { } else {
scope.state._buttonText = 'REPLACE'; scope.state._buttonText = 'REPLACE';
scope.state._placeholder = 'ENCRYPTED';
vm.toggle = vm.toggleRevertReplace; vm.toggle = vm.toggleRevertReplace;
} }

View File

@@ -3,7 +3,7 @@
<at-input-label></at-input-label> <at-input-label></at-input-label>
<div class="input-group"> <div class="input-group">
<span class="input-group-btn"> <span class="input-group-btn at-InputGroup-button">
<button class="btn at-ButtonHollow--white at-Input-button" <button class="btn at-ButtonHollow--white at-Input-button"
ng-disabled="!state._enableToggle && (state._disabled || form.disabled)" ng-disabled="!state._enableToggle && (state._disabled || form.disabled)"
ng-click="vm.toggle()"> ng-click="vm.toggle()">
@@ -16,7 +16,7 @@
ng-class="{ 'at-Input--rejected': state._rejected }" ng-class="{ 'at-Input--rejected': state._rejected }"
ng-attr-maxlength="{{ state.max_length || undefined }}" ng-attr-maxlength="{{ state.max_length || undefined }}"
ng-attr-tabindex="{{ tab || undefined }}" ng-attr-tabindex="{{ tab || undefined }}"
ng-attr-placeholder="{{::state._placeholder || undefined }}" ng-attr-placeholder="{{state._placeholder || undefined }}"
ng-change="vm.check()" ng-change="vm.check()"
ng-disabled="state._disabled || form.disabled" /> ng-disabled="state._disabled || form.disabled" />
</div> </div>

View File

@@ -1,3 +1,5 @@
const DEFAULT_EMPTY_PLACEHOLDER = 'NO OPTIONS AVAILABLE';
function atInputSelectLink (scope, element, attrs, controllers) { function atInputSelectLink (scope, element, attrs, controllers) {
let formController = controllers[0]; let formController = controllers[0];
let inputController = controllers[1]; let inputController = controllers[1];
@@ -25,6 +27,11 @@ function AtInputSelectController (baseInputController, eventService) {
input = element.find('input')[0]; input = element.find('input')[0];
select = element.find('select')[0]; select = element.find('select')[0];
if (!scope.state._data || scope.state._data.length === 0) {
scope.state._disabled = true;
scope.state._placeholder = DEFAULT_EMPTY_PLACEHOLDER;
}
vm.setListeners(); vm.setListeners();
vm.check(); vm.check();
@@ -55,6 +62,8 @@ function AtInputSelectController (baseInputController, eventService) {
vm.updateDisplayModel = () => { vm.updateDisplayModel = () => {
if (scope.state._format === 'array') { if (scope.state._format === 'array') {
scope.displayModel = scope.state._data[scope.state._value]; scope.displayModel = scope.state._data[scope.state._value];
} else if (scope.state._format === 'objects') {
scope.displayModel = scope.state._value[scope.state._display];
} else if (scope.state._format === 'grouped-object') { } else if (scope.state._format === 'grouped-object') {
scope.displayModel = scope.state._value[scope.state._display]; scope.displayModel = scope.state._value[scope.state._display];
} else { } else {

View File

@@ -34,6 +34,7 @@ function AtInputTextareaSecretController (baseInputController, eventService) {
if (scope.state._value) { if (scope.state._value) {
scope.state._buttonText = 'REPLACE'; scope.state._buttonText = 'REPLACE';
scope.state._placeholder = 'ENCRYPTED';
} else { } else {
if (scope.state.format === 'ssh_private_key') { if (scope.state.format === 'ssh_private_key') {
vm.listeners = vm.setFileListeners(textarea, input); vm.listeners = vm.setFileListeners(textarea, input);
@@ -48,10 +49,12 @@ function AtInputTextareaSecretController (baseInputController, eventService) {
vm.toggleRevertReplace(); vm.toggleRevertReplace();
if (scope.state._isBeingReplaced) { if (scope.state._isBeingReplaced) {
scope.state._placeholder = '';
scope.state._displayHint = true; scope.state._displayHint = true;
vm.listeners = vm.setFileListeners(textarea, input); vm.listeners = vm.setFileListeners(textarea, input);
} else { } else {
scope.state._displayHint = false; scope.state._displayHint = false;
scope.state._placeholder = 'ENCRYPTED';
eventService.remove(vm.listeners); eventService.remove(vm.listeners);
} }
}; };

View File

@@ -18,9 +18,10 @@
<textarea class="form-control at-Input at-InputTextarea" <textarea class="form-control at-Input at-InputTextarea"
ng-model="state[state._activeModel]" ng-model="state[state._activeModel]"
ng-class="{ 'at-Input--rejected': state._rejected }" ng-class="{ 'at-Input--rejected': state._rejected }"
ng-attr-rows="{{::state._rows || 6 }}"
ng-attr-maxlength="{{ state.max_length || undefined }}" ng-attr-maxlength="{{ state.max_length || undefined }}"
ng-attr-tabindex="{{ tab || undefined }}" ng-attr-tabindex="{{ tab || undefined }}"
ng-attr-placeholder="{{::state._placeholder || undefined }}" ng-attr-placeholder="{{state._placeholder || undefined }}"
ng-change="vm.check()" ng-change="vm.check()"
ng-disabled="state._disabled || form.disabled" /> ng-disabled="state._disabled || form.disabled" />
</textarea> </textarea>

View File

@@ -5,6 +5,7 @@
<textarea class="form-control at-Input at-InputTextarea" <textarea class="form-control at-Input at-InputTextarea"
ng-model="state._value" ng-model="state._value"
ng-class="{ 'at-Input--rejected': state._rejected }" ng-class="{ 'at-Input--rejected': state._rejected }"
ng-attr-rows="{{::state._rows || 6 }}"
ng-attr-maxlength="{{ state.max_length || undefined }}" ng-attr-maxlength="{{ state.max_length || undefined }}"
ng-attr-tabindex="{{ tab || undefined }}" ng-attr-tabindex="{{ tab || undefined }}"
ng-attr-placeholder="{{::state._placeholder || undefined }}" ng-attr-placeholder="{{::state._placeholder || undefined }}"

View File

@@ -21,4 +21,5 @@
.at-Panel-headingTitle { .at-Panel-headingTitle {
.at-mixin-Heading(@at-font-size-3x); .at-mixin-Heading(@at-font-size-3x);
text-transform: none;
} }

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="col-xs-2"> <div class="col-xs-2">
<div class="at-Panel-dismiss"> <div class="at-Panel-dismiss">
<i class="fa fa-times fa-lg" ng-click="dismiss()"></i> <i class="fa fa-times-circle fa-lg" ng-click="dismiss()"></i>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -39,7 +39,7 @@
} }
.at-Popover-title { .at-Popover-title {
.at-mixin-Heading(@at-font-size-2x); .at-mixin-Heading(@at-font-size);
color: @at-white; color: @at-white;
margin-bottom: @at-space-4x; margin-bottom: @at-space-4x;
} }
@@ -47,5 +47,5 @@
.at-Popover-text { .at-Popover-text {
margin: 0; margin: 0;
padding: 0; padding: 0;
line-height: 1; font-size: @at-font-size;
} }

View File

@@ -4,13 +4,14 @@
.at-Tab { .at-Tab {
margin: 0 @at-space-5x 0 0; margin: 0 @at-space-5x 0 0;
font-size: @at-font-size;
} }
.at-Tab--active { .at-Tab--active {
&, &:hover, &:active, &:focus { &, &:hover, &:active, &:focus {
color: @at-white; color: @at-white;
background-color: @at-gray-dark-2x; background-color: @at-gray-dark-3x;
border-color: @at-gray-dark-2x; border-color: @at-gray-dark-3x;
cursor: default; cursor: default;
} }
} }
@@ -18,8 +19,8 @@
.at-Tab--disabled { .at-Tab--disabled {
&, &:hover, &:active, &:focus { &, &:hover, &:active, &:focus {
background-color: @at-white; background-color: @at-white;
color: @at-gray-dark-3x; color: @at-gray-dark-2x;
border-color: @at-gray-dark-3x; border-color: @at-gray-dark-2x;
opacity: 0.65; opacity: 0.65;
cursor: not-allowed; cursor: not-allowed;
} }

View File

@@ -128,10 +128,17 @@ function normalizePath (resource) {
return `${version}${resource}/`; return `${version}${resource}/`;
} }
function getById (id) {
let item = this.get('results').filter(result => result.id === id);
return item ? item[0] : undefined;
}
function BaseModel (path) { function BaseModel (path) {
this.model = {}; this.model = {};
this.get = get; this.get = get;
this.normalizePath = normalizePath; this.normalizePath = normalizePath;
this.getById = getById;
this.request = request; this.request = request;
this.http = { this.http = {
get: httpGet.bind(this), get: httpGet.bind(this),

View File

@@ -26,18 +26,11 @@ function mergeInputProperties (type) {
}); });
} }
function getById (id) {
let type = this.get('results').filter(type => type.id === id);
return type ? type[0] : undefined;
}
function CredentialTypeModel (method, id) { function CredentialTypeModel (method, id) {
BaseModel.call(this, 'credential_types'); BaseModel.call(this, 'credential_types');
this.categorizeByKind = categorizeByKind.bind(this); this.categorizeByKind = categorizeByKind.bind(this);
this.mergeInputProperties = mergeInputProperties.bind(this); this.mergeInputProperties = mergeInputProperties.bind(this);
this.getById = getById.bind(this);
return this.request(method, id) return this.request(method, id)
.then(() => this); .then(() => this);

View File

@@ -0,0 +1,18 @@
let BaseModel;
function OrganizationModel (method) {
BaseModel.call(this, 'organizations');
return this.request(method)
.then(() => this);
}
function OrganizationModelLoader (_BaseModel_) {
BaseModel = _BaseModel_;
return OrganizationModel;
}
OrganizationModelLoader.$inject = ['BaseModel'];
export default OrganizationModelLoader;

View File

@@ -2,11 +2,13 @@ import Base from './Base';
import Credential from './Credential'; import Credential from './Credential';
import CredentialType from './CredentialType'; import CredentialType from './CredentialType';
import Me from './Me'; import Me from './Me';
import Organization from './Organization';
angular angular
.module('at.lib.models', []) .module('at.lib.models', [])
.service('BaseModel', Base) .service('BaseModel', Base)
.service('CredentialModel', Credential) .service('CredentialModel', Credential)
.service('CredentialTypeModel', CredentialType) .service('CredentialTypeModel', CredentialType)
.service('MeModel', Me); .service('MeModel', Me)
.service('OrganizationModel', Organization);

View File

@@ -10,6 +10,10 @@
.at-Button--green { .at-Button--green {
.at-mixin-Button(); .at-mixin-Button();
.at-mixin-ButtonColor('at-green', 'at-white'); .at-mixin-ButtonColor('at-green', 'at-white');
&[disabled] {
background: @at-gray-dark;
}
} }
.at-Button--blue { .at-Button--blue {
@@ -25,6 +29,7 @@
.at-ButtonHollow--white { .at-ButtonHollow--white {
.at-mixin-Button(); .at-mixin-Button();
.at-mixin-ButtonHollow('at-gray-dark-3x', 'at-gray-dark-2x'); .at-mixin-ButtonHollow('at-gray-dark-3x', 'at-gray-dark-2x');
border-color: @at-gray-dark;
} }
.at-ButtonIcon { .at-ButtonIcon {

View File

@@ -23,6 +23,7 @@
.at-mixin-Button () { .at-mixin-Button () {
height: @at-input-height; height: @at-input-height;
padding: @at-space-2x @at-space-4x; padding: @at-space-2x @at-space-4x;
font-size: @at-font-size;
} }
.at-mixin-ButtonColor (@background, @color, @hover: '@{background}--hover') { .at-mixin-ButtonColor (@background, @color, @hover: '@{background}--hover') {

View File

@@ -50,6 +50,7 @@
@at-font-weight-3x: 900; @at-font-weight-3x: 900;
@at-line-height-short: 0.9; @at-line-height-short: 0.9;
@at-line-height-tall: 2;
@at-line-height: 24px; @at-line-height: 24px;
// 3. Layout -------------------------------------------------------------------------------------- // 3. Layout --------------------------------------------------------------------------------------
@@ -68,3 +69,4 @@
@at-border-radius: 5px; @at-border-radius: 5px;
@at-popover-width: 320px; @at-popover-width: 320px;
@at-inset-width: 5px; @at-inset-width: 5px;