diff options
| author | miramurali23 <miramurali23@gmail.com> | 2019-06-17 13:07:24 -0500 |
|---|---|---|
| committer | Gunnar Mills <gmills@us.ibm.com> | 2019-09-26 20:04:56 +0000 |
| commit | afc8a799627b71bba716e207cee8185852a6d390 (patch) | |
| tree | 69a9bf15b0603a51bf8194c218aba6a0d20e5409 /app/configuration | |
| parent | 5e258e43070b46b9d1ec5ec01e02b9f707cbf7b8 (diff) | |
| download | phosphor-webui-afc8a799627b71bba716e207cee8185852a6d390.tar.gz phosphor-webui-afc8a799627b71bba716e207cee8185852a6d390.zip | |
Update users navigation section
- Changed the section name to be access-control
- Moved LDAP Settings and Certificate Management to access-control navigation
- Changed Manage User Account subsection name to Local User Management
Resolves: openbmc/phosphor-webui#619
Signed-off-by: Mira Murali <miramurali23@gmail.com>
Signed-off-by: Derick Montague <derick.montague@ibm.com>
Change-Id: I0d94c80c295b997d94c04330fd87f4fc4d229bf8
Diffstat (limited to 'app/configuration')
| -rw-r--r-- | app/configuration/controllers/certificate-controller.html | 294 | ||||
| -rw-r--r-- | app/configuration/controllers/certificate-controller.js | 238 | ||||
| -rw-r--r-- | app/configuration/controllers/ldap-controller.html | 148 | ||||
| -rw-r--r-- | app/configuration/controllers/ldap-controller.js | 224 | ||||
| -rw-r--r-- | app/configuration/index.js | 11 | ||||
| -rw-r--r-- | app/configuration/styles/certificate.scss | 250 | ||||
| -rw-r--r-- | app/configuration/styles/index.scss | 2 | ||||
| -rw-r--r-- | app/configuration/styles/ldap.scss | 269 |
8 files changed, 0 insertions, 1436 deletions
diff --git a/app/configuration/controllers/certificate-controller.html b/app/configuration/controllers/certificate-controller.html deleted file mode 100644 index 4226262..0000000 --- a/app/configuration/controllers/certificate-controller.html +++ /dev/null @@ -1,294 +0,0 @@ -<loader loading="loading"></loader> -<div id="configuration-cert"> - <div class="row column"> - <h1>SSL certificates</h1> - </div> - <div ng-repeat="certificate in certificates | filter:{isExpiring:true}" class="row column"> - <div class="small-12 alert alert-warning" role="alert"> - <icon file="icon-warning.svg" aria-hidden="true"></icon> - The uploaded {{certificate.name}} is expiring in {{getDays(certificate.ValidNotAfter) === 0 ? 'less than one day!' : getDays(certificate.ValidNotAfter) - + ' days!'}} Consider replacing it with a new certificate. - </div> - </div> - <div ng-repeat="certificate in certificates|filter:{isExpired:true}" class="row column"> - <div class="small-12 alert alert-danger" role="alert"> - <div class="icon__critical inline"></div> The uploaded {{certificate.name}} has expired! Consider replacing it with a new certificate. - </div> - </div> - <div class="row column"> - <button type="button" class="btn btn-tertiary" ng-disabled="availableCertificateTypes.length === 0" ng-click="addCertificateModal=true"> - <icon class="icon-add" file="icon-plus.svg"></icon> - Add new certificate - </button> - <button type="button" class="btn btn-tertiary" ng-click="addCSRModal=true"> - <icon class="icon-add" file="icon-plus.svg"></icon> - Generate CSR - </button> - </div> - <div class="row column"> - <div class="small-12 certificate__table"> - <div class="table__row-header"> - <div class="row column"> - <div class="certificate__type-header"> - Certificate - </div> - <div class="certificate__issue-header"> - Issued by - </div> - <div class="certificate__issue-header"> - Issued to - </div> - <div class="certificate__date-header"> - Valid from - </div> - <div class="certificate__status-header"> - </div> - <div class="certificate__date-header"> - Valid until - </div> - </div> - </div> - <div ng-if="certificates.length < 1" class="empty__logs">There have been no certificates added.</div> - <div ng-repeat="certificate in certificates"> - <certificate cert="certificate" reload="loadCertificates()" )></certificate> - </div> - </div> - </div> - <section class="modal add__certificate__modal" aria-hidden="true" role="dialog" ng-class="{'active': addCertificateModal}"> - <form name="add__cert__form" id="add__cert__form" ng-class="{'submitted': submitted}"> - <div class="modal__content"> - <button class="certificate__close-modal" ng-click="addCertificateModal = false; add__cert__form.$setUntouched()"> - <icon aria-hidden="true" file="icon-close.svg"> - </button> - <h2 class="page-header">Add new certificate</h2> - <div class="row column add-certificate__section "> - <div class="small-12"> - <label for="cert__type">Certificate type</label> - <select id="cert__type" name="cert__type" ng-model="newCertificate.selectedType" required> - <option class="courier-bold" ng-value="">Select an option</option> - <option class="courier-bold" ng-value="type" ng-repeat="type in availableCertificateTypes"> - {{type.name}}</option> - </select> - <div ng-messages="add__cert__form.cert__type.$error" class="form-error" ng-class="{'visible' : add__cert__form.cert__type.$touched || submitted }"> - <p ng-message="required">Field is required</p> - </div> - </div> - </div> - <div class="row column add-certificate__section"> - <div class="small-12"> - <label class="select__new-label" for="upload_cert_new">Certificate file</label> - </div> - <div class="row column file__upload add-certificate__section "> - <label for='upload_cert_new'> - <input name="upload_cert_new" id="upload_cert_new" type="file" file="newCertificate.file" class="hide" /> - <span class="btn btn-secondary select__new-button">Choose file</span> - </label> - </div> - <div class="row column add-certificate__section "> - <div ng-if="newCertificate.file" class="small-7 file__name"> - <span>{{newCertificate.file.name}}</span> - <icon file="icon-close.svg" ng-if="newCertificate.file.name" ng-click="newCertificate.file = '';" class="float-right"></icon> - </div> - </div> - </div> - </div> - <div class="modal__button-wrapper"> - <button class="btn btn-secondary" ng-click="addCertificateModal = false; newCertificate={};add__cert__form.$setUntouched();">Cancel</button> - <button class="btn btn-primary" ng-disabled="add__cert__form.$invalid || !newCertificate.file" ng-click="submitted = true; uploadCertificate();">Save</button> - </div> - </form> - </section> - - <section class="modal add-csr__modal" aria-hidden="true" role="dialog" ng-class="{'active': addCSRModal}"> - <!--Close button for displaying CSR Code, we need a close button within form to reset form validation correctly--> - <button class="certificate__close-modal" ng-click="resetCSRModal();" ng-if="displayCSRCode==true"> - <icon aria-hidden="true" file="icon-close.svg"> - </button> - - <!-- CSR Code display content--> - - <div ng-if="displayCSRCode==true"> - <h2 class="page-header">Certificate Signing Request (CSR)</h2> - <div class="modal__content add-csr__container"> - <span id="csrCode" class="add-csr__container-csr-code">{{csrCode}}</span> - </div> - <div class="modal__button-wrapper"> - <button class="btn btn-secondary" clipboard text="csrCode" on-copied="copySuccess(event)" on-error="copyfailed(err)"> - <span ng-if="!copied">Copy</span> - <span ng-if="copied"> - <icon aria-hidden="true" file="icon-check.svg"></icon> - <span>Copied</span> - </span> - </button> - <button class="btn btn-primary" ng-click="addCSRModal = false;"> - <a ng-href="data:text/json;charset=utf-8,{{csrCode}}" download="csrCode.txt" class="add-csr__text-download"> - Download</a> - </button> - </div> - - </div> - - - - <form name="add__csr__form" id="add__csr__form" novalidate ng-if="displayCSRCode==false"> - <div class="modal__content add-csr__container"> - <button class="certificate__close-modal" ng-click="resetCSRModal(); add__csr__form.$setUntouched()"> - <icon aria-hidden="true" file="icon-close.svg"> - </button> - <h2 class="page-header">Generate a Certificate Signing Request (CSR)</h2> - <div class="row"> - <fieldset class="column medium-8 add-csr__section"> - <legend class="add-csr__section-title">General</legend> - <div class="row"> - <div class="column medium-6"> - <label for="cert__type" class="add-csr__label">Certificate Type *</label> - <select class="add-csr__select" id="cert__type" name="cert__type" ng-model="newCSR.certificateCollection" required> - <option class="courier-bold" ng-value="default" ng-model="selectOption">Select an option</option> - <!-- Do not show CA certificate as an option. Only a certificate authority can generate a CA certificate (known as TrustStore Certificate in Redfish) --> - <option class="courier-bold" ng-value="type" ng-repeat="type in allCertificateTypes" ng-if="type.Description !== 'TrustStore Certificate'"> - {{type.name}}</option> - </select> - <div ng-messages="add__csr__form.cert__type.$error" class="form-error" ng-class="{'visible' : add__csr__form.cert__type.$touched}"> - <p ng-message="required">Field is required</p> - </div> - </div> - <div class="column medium-6"> - <label for="countryCode" class="add-csr__label">Country *</label> - <select class="add-csr__select" id="countryCode" name="countryCode" ng-model="newCSR.countryCode" required> - <option class="courier-bold" ng-value="" ng-model="selectOption">Select an option</option> - <option class="courier-bold" ng-value="country" ng-repeat="country in countryList">{{country.Name}} - </option> - </select> - <div ng-messages="add__csr__form.countryCode.$error" class="form-error" ng-class="{'visible' : add__csr__form.countryCode.$touched}"> - <p ng-message="required">Field is required</p> - </div> - </div> - - <div class="column medium-6"> - <label for="state" class="add-csr__label">State *</label> - <input class="add-csr__input" ng-model="newCSR.state" type="text" id="state" name="state" required></input> - <div ng-messages="add__csr__form.state.$error" class="form-error" ng-class="{'visible' : add__csr__form.state.$touched}"> - <p ng-message="required">Field is required</p> - </div> - </div> - - <div class="column medium-6"> - <label for="city" class="add-csr__label">City *</label> - <input class="add-csr__input" id="city" name="city" ng-model="newCSR.city" type="text" required></input> - <div ng-messages="add__csr__form.city.$error" class="form-error" ng-class="{'visible' : add__csr__form.city.$touched}"> - <p ng-message="required">Field is required</p> - </div> - </div> - - <div class="column medium-6"> - <label for="companyName" class="add-csr__label">Company Name *</label> - <input class="add-csr__input" type="text" ng-model="newCSR.organization" id="companyName" name="companyName" required></input> - <div ng-messages="add__csr__form.companyName.$error" class="form-error" ng-class="{'visible' : add__csr__form.companyName.$touched}"> - <p ng-message="required">Field is required</p> - </div> - </div> - - <div class="column medium-6"> - <label for="companyUnit" class="add-csr__label">Company Unit *</label> - <input class="add-csr__input" ng-model="newCSR.companyUnit" name="companyUnit" id="companyUnit" type="text" required></input> - <div ng-messages="add__csr__form.companyUnit.$error" class="form-error" ng-class="{'visible' : add__csr__form.companyUnit.$touched}"> - <p ng-message="required">Field is required</p> - </div> - </div> - - <div class="column medium-6"> - <label for="commonName" class="add-csr__label">Common Name *</label> - <input class="add-csr__input" ng-model="newCSR.commonName" name="commonName" type="text" id="commonName" required></input> - <div ng-messages="add__csr__form.commonName.$error" class="form-error" ng-class="{'visible' : add__csr__form.commonName.$touched}"> - <p ng-message="required">Field is required</p> - </div> - </div> - - <div class="column medium-6"> - <label for="challengePassword" class="add-csr__label">Challenge Password</label> - <input class="add-csr__input-no-validation" id="challengePassword" ng-model="newCSR.challengePassword" type="text"></input> - </div> - - <div class="column medium-6"> - <label for="contactPerson" class="add-csr__label">Contact Person</label> - <input class="add-csr__input-no-validation" id="contactPerson" ng-model="newCSR.contactPerson" type="text"></input> - </div> - - <div class="column medium-6"> - <label for="emailAddress" class="add-csr__label">Email Address</label> - <input class="add-csr__input-no-validation" id="emailAddress" ng-model="newCSR.emailAddress" type="text"></input> - </div> - - <div class="column medium-6"> - <div> - <label id="alternate-name-label" for="alternateName" class="add-csr__label">Alternate Name</label> - <input class="add-csr__input-no-validation" ng-model="newCSR.firstAlternativeName" id="alternateName" name="alternativeName" - type="text"></input> - </div> - <div class="add-csr__additional-alt-names" ng-repeat="name in names"> - <input id="alternate-name-input-{{$index}}" aria-describedby="alternate-name-label" class="add-csr__input-no-validation" - ng-model="name.Value" type="text"></input> - <button aria-label="Delete alternate name field" aria-controls="alternate-name-input-{{$index}}" class="btn btn-tertiary add-csr__alt-name-delete-btn" - ng-click="deleteOptionalRow($index)" ng-disabled="multiSelected"> - <icon aria-hidden="true" file="icon-trashcan.svg"> - </button> - </div> - </div> - - <div class="column medium-6"> - <button class="btn btn-tertiary add-csr__alt-name-add-btn" ng-click="addOptionalRow()"> - <icon file="icon-plus.svg"></icon> - Add another alternate name - </button> - </div> - </div> - </fieldset> - - <fieldset class="column medium-4 add-csr__section add-csr__section--border "> - <legend class="add-csr__section-title">Private key</legend> - <div class="add-csr__container-private-key"> - <div class="add-csr__content-private-key"> - <label for="keyPairAlgorithm" class="add-csr__label">Key Pair Algorithm *</label> - <select class="add-csr__select" ng-model="newCSR.keyPairAlgorithm" id="keyPairAlgorithm" name="keyPairAlgorithm" required> - <option class="courier-bold" ng-value="" ng-model="selectOption">Select an option</option> - <option class="courier-bold" ng-value="data" ng-repeat="data in keyPairAlgorithm">{{data}}</option> - </select> - <div ng-messages="add__csr__form.keyPairAlgorithm.$error" class="form-error" ng-class="{'visible' : add__csr__form.keyPairAlgorithm.$touched}"> - <p ng-message="required">Field is required</p> - </div> - - <div ng-if="newCSR.keyPairAlgorithm == 'EC'"> - <label for="keyCurveId" class="add-csr__label">Key Curve ID</label> - <select class="add-csr__select" ng-model="newCSR.keyCurveId" id="keyCurveId" name="keyCurveId" required> - <option class="courier-bold" ng-value="">None</option> - <option class="courier-bold" ng-value="data" ng-repeat="data in keyCurveId">{{data}}</option> - </select> - <div ng-messages="add__csr__form.keyCurveId.$error" class="form-error" ng-class="{'visible' : add__csr__form.keyCurveId.$touched}"> - <p ng-message="required">Field is required</p> - </div> - </div> - - - <div ng-if="newCSR.keyPairAlgorithm =='RSA'"> - <label for="keyBitLength" class="add-csr__label">Key Bit Length *</label> - <select class="add-csr__select" ng-model="newCSR.keyBitLength" id="keyBitLength" name="keyBitLength" required> - <option class="courier-bold" ng-value="">Select an option</option> - <option class="courier-bold" ng-value="data" ng-repeat="data in keyBitLength">{{data}}</option> - </select> - <div ng-messages="add__csr__form.keyBitLength.$error" class="form-error" ng-class="{'visible' : add__csr__form.keyBitLength.$touched}"> - <p ng-message="required">Field is required</p> - </div> - </div> - - </div> - </fieldset> - </div> - </div> - <div class="modal__button-wrapper"> - <button class="btn btn-secondary" ng-click="resetCSRModal();add__csr__form.$setUntouched();">Cancel</button> - <button class="btn btn-primary" ng-click="csrSubmitted = true; getCSRCode();add__csr__form.$setUntouched();" ng-disabled="add__csr__form.$invalid">Generate CSR</button> - </div> - </div> - </form> - </section> - <div class="modal-overlay" tabindex="-1" ng-class="{'active': addCertificateModal || addCSRModal}"></div>
\ No newline at end of file diff --git a/app/configuration/controllers/certificate-controller.js b/app/configuration/controllers/certificate-controller.js deleted file mode 100644 index 06b2fff..0000000 --- a/app/configuration/controllers/certificate-controller.js +++ /dev/null @@ -1,238 +0,0 @@ -/** - * Controller for Certificate Management - * - * @module app/configuration - * @exports certificateController - * @name certificateController - */ - -window.angular && (function(angular) { - 'use strict'; - - angular.module('app.configuration').controller('certificateController', [ - '$scope', 'APIUtils', '$q', 'Constants', 'toastService', '$timeout', - function($scope, APIUtils, $q, Constants, toastService, $timeout) { - $scope.loading = false; - $scope.certificates = []; - $scope.availableCertificateTypes = []; - $scope.allCertificateTypes = Constants.CERTIFICATE_TYPES; - $scope.addCertificateModal = false; - $scope.addCSRModal = false; - $scope.newCertificate = {}; - $scope.newCSR = {}; - $scope.submitted = false; - $scope.csrSubmitted = false; - $scope.csrCode = ''; - $scope.displayCSRCode = false; - $scope.keyBitLength = Constants.CERTIFICATE.KEY_BIT_LENGTH; - $scope.keyPairAlgorithm = Constants.CERTIFICATE.KEY_PAIR_ALGORITHM; - $scope.keyCurveId = Constants.CERTIFICATE.KEY_CURVE_ID; - $scope.countryList = Constants.COUNTRIES; - - - $scope.$on('$viewContentLoaded', () => { - getBmcTime(); - }) - - $scope.loadCertificates = function() { - $scope.certificates = []; - $scope.availableCertificateTypes = Constants.CERTIFICATE_TYPES; - $scope.loading = true; - // Use Certificate Service to get the locations of all the certificates, - // then add a promise for fetching each certificate - APIUtils.getCertificateLocations().then( - function(data) { - var promises = []; - var locations = data.Links.Certificates; - for (var i in locations) { - var location = locations[i]; - promises.push(getCertificatePromise(location['@odata.id'])); - } - $q.all(promises) - .catch(function(error) { - toastService.error('Failed to load certificates.'); - console.log(JSON.stringify(error)); - }) - .finally(function() { - $scope.loading = false; - }); - }, - function(error) { - $scope.loading = false; - $scope.availableCertificateTypes = []; - toastService.error('Failed to load certificates.'); - console.log(JSON.stringify(error)); - }); - }; - - $scope.uploadCertificate = function() { - if ($scope.newCertificate.file.name.split('.').pop() !== 'pem') { - toastService.error('Certificate must be a .pem file.'); - return; - } - $scope.addCertificateModal = false; - APIUtils - .addNewCertificate( - $scope.newCertificate.file, $scope.newCertificate.selectedType) - .then( - function(data) { - toastService.success( - $scope.newCertificate.selectedType.name + - ' was uploaded.'); - $scope.newCertificate = {}; - $scope.loadCertificates(); - }, - function(error) { - toastService.error( - $scope.newCertificate.selectedType.name + - ' failed upload.'); - console.log(JSON.stringify(error)); - }); - }; - - var getCertificatePromise = function(url) { - var promise = APIUtils.getCertificate(url).then(function(data) { - var certificate = data; - isExpiring(certificate); - updateAvailableTypes(certificate); - $scope.certificates.push(certificate); - }); - return promise; - }; - - var isExpiring = function(certificate) { - // convert certificate time to epoch time - // if ValidNotAfter is less than or equal to 30 days from bmc time - // (2592000000), isExpiring. If less than or equal to 0, is expired. - // dividing bmc time by 1000 converts epoch milliseconds to seconds - var difference = (new Date(certificate.ValidNotAfter).getTime()) - - ($scope.bmcTime) / 1000; - if (difference <= 0) { - certificate.isExpired = true; - } else if (difference <= 2592000000) { - certificate.isExpiring = true; - } else { - certificate.isExpired = false; - certificate.isExpiring = false; - } - }; - - // add optional name - $scope.names = []; - $scope.addOptionalRow = function() { - $scope.names.push({Value: ''}) - }; - - // remove optional name row - $scope.deleteOptionalRow = function(index) { - $scope.names.splice(index, 1); - if ($scope.names.length == 0) { - $scope.names = []; - } - }; - - - // create a CSR object to send to the backend - $scope.getCSRCode = function() { - var addCSR = {}; - let alternativeNames = $scope.names.map(name => name.Value); - - // if user provided a first alternative name then push to alternative - // names array - $scope.newCSR.firstAlternativeName ? - alternativeNames.push($scope.newCSR.firstAlternativeName) : - $scope.newCSR.firstAlternativeName = ''; - - - addCSR.CertificateCollection = { - '@odata.id': $scope.newCSR.certificateCollection.location - }; - addCSR.CommonName = $scope.newCSR.commonName; - addCSR.ContactPerson = $scope.newCSR.contactPerson || ''; - addCSR.City = $scope.newCSR.city; - addCSR.AlternativeNames = alternativeNames || []; - addCSR.ChallengePassword = $scope.newCSR.challengePassword || ''; - addCSR.Email = $scope.newCSR.emailAddress || ''; - addCSR.Country = $scope.newCSR.countryCode.code; - addCSR.Organization = $scope.newCSR.organization; - addCSR.OrganizationalUnit = $scope.newCSR.companyUnit; - addCSR.KeyCurveId = $scope.newCSR.keyCurveId || ''; - addCSR.KeyBitLength = $scope.newCSR.keyBitLength - addCSR.KeyPairAlgorithm = $scope.newCSR.keyPairAlgorithm || ''; - addCSR.State = $scope.newCSR.state; - - APIUtils.createCSRCertificate(addCSR).then( - function(data) { - $scope.displayCSRCode = true; - $scope.csrCode = data; - }, - function(error) { - $scope.addCSRModal = false; - toastService.error('Unable to generate CSR. Try again.'); - console.log(JSON.stringify(error)); - }) - }; - - // resetting the modal when user clicks cancel/closes the - // modal - $scope.resetCSRModal = function() { - $scope.addCSRModal = false; - $scope.displayCSRCode = false; - $scope.newCSR.certificateCollection = $scope.selectOption; - $scope.newCSR.commonName = ''; - $scope.newCSR.contactPerson = ''; - $scope.newCSR.city = ''; - $scope.names = []; - $scope.newCSR.challengePassword = ''; - $scope.newCSR.emailAddress = ''; - $scope.newCSR.countryCode = ''; - $scope.newCSR.keyCurveId = ''; - $scope.newCSR.firstAlternativeName = ''; - $scope.newCSR.keyBitLength = $scope.selectOption; - $scope.newCSR.keyPairAlgorithm = $scope.selectOption; - $scope.newCSR.organization = ''; - $scope.newCSR.companyUnit = ''; - $scope.newCSR.state = ''; - }; - - // copies the CSR code - $scope.copySuccess = function(event) { - $scope.copied = true; - $timeout(function() { - $scope.copied = false; - }, 5000); - }; - $scope.copyFailed = function(err) { - console.log(JSON.stringify(err)); - }; - - - var getBmcTime = function() { - APIUtils.getBMCTime().then(function(data) { - $scope.bmcTime = data.data.Elapsed; - }); - - return $scope.bmcTime; - }; - - var updateAvailableTypes = function(certificate) { - // TODO: at this time only one of each type of certificate is allowed. - // When this changes, this will need to be updated. - // Removes certificate type from available types to be added. - $scope.availableCertificateTypes = - $scope.availableCertificateTypes.filter(function(type) { - return type.Description !== certificate.Description; - }); - }; - - $scope.getDays = function(endDate) { - // finds number of days until certificate expiration - // dividing bmc time by 1000 converts milliseconds to seconds - var ms = (new Date(endDate).getTime()) - ($scope.bmcTime) / 1000; - return Math.floor(ms / (24 * 60 * 60 * 1000)); - }; - - $scope.loadCertificates(); - } - ]); -})(angular); diff --git a/app/configuration/controllers/ldap-controller.html b/app/configuration/controllers/ldap-controller.html deleted file mode 100644 index daace8e..0000000 --- a/app/configuration/controllers/ldap-controller.html +++ /dev/null @@ -1,148 +0,0 @@ -<loader loading="loading"></loader> -<div class="ldap" id="configuration-ldap"> - <div class="row column"> - <h1>LDAP role group settings</h1> - </div> - <div class="row column"> - <p>Configure LDAP settings and manage role groups.</p> - </div> - <div class="row column"> - <h2 class="subhead"> - Settings - </h2> - </div> - <div class="row column"> - <label class="control-check ldap__control-check"> - <input type="checkbox" id="enable-ldap-checkbox" - ng-change="updateServiceEnabled(); ldap__configuration.$setUntouched()" - ng-model="ldapProperties.ServiceEnabled" /> - <span class="control__indicator"></span> - <span class="control__label"> - <strong>Enable LDAP authentication</strong> <br> - LDAP authentication must be enabled to modify role groups. - </span> - </label> - </div> - <div class="row column"> - <form id="ldap__configuration" name="ldap__configuration" ng-class="{'submitted': submitted}" - class="ldap__configuration" novalidate> - <fieldset ng-disabled="!ldapProperties.ServiceEnabled"> - <div class="ldap__configure-settings row column"> - <div class="large-3 column ldap__ssl-column"> - <label class="control-check" ng-class="{'disabled' : certificates.length < 1}"> - <input id="secure-ldap-ssl" type="checkbox" ng-model="ldapProperties.useSSL" - ng-checked="ldapProperties.useSSL" ng-disabled="certificates.length < 1" /> - <span class="control__indicator"></span> - <span class="control__label">Secure LDAP using SSL</span> - </label> - <div> - <div class="ldap__certificate-info" ng-if="ldapProperties.ServiceEnabled"> - <p>Client certificate valid until:</p> - <small> - {{clientCertificateExpires ? (clientCertificateExpires | localeDate) : 'none available'}}</small> - </div> - </div> - <div class="ldap__certificate-info" ng-if="data.ValidNotAfter='' || !ldapProperties.ServiceEnabled"> - <span>SSL certificates must be uploaded to secure LDAP using SSL.</span> - </div> - <div class="ldap__certificate-info"> - <a href="#/configuration/certificate">Go to SSL certificates</a> - </div> - </div> - <div class="large-9 columns ldap__server-info"> - <div class="column service-type-column"> - <fieldset class="ldap__server-info-service-type"> - <legend class="content-label">Service Type</legend> - <label class="control-radio control__radio__label" for="open-ldap">Open LDAP - <input type="radio" name="service_enabled_type" id="open-ldap" value="ldap" - ng-checked="ldapProperties.LDAPServiceEnabled" - ng-change="ldapProperties.EnabledServiceUpdated = true" ng-model="ldapProperties.EnabledServiceType" - required /> - <span class="control__indicator control__indicator-on control__indicator-service-type"></span> - </label> - <label class="control-radio control__radio__label" for="active-directory">Active directory - <input type="radio" name="service_enabled_type" id="active-directory" - ng-change="ldapProperties.EnabledServiceUpdated = true" value="ad" - ng-checked="ldapProperties.ADServiceEnabled" ng-model="ldapProperties.EnabledServiceType" - required /> - <span class="control__indicator control__indicator-on control__indicator-service-type"></span> - </label> - </fieldset> - </div> - <div class="medium-6 large-4 columns"> - <label for="ldap__uri">Server uri</label> - <input id="ldap__uri" name="ldap__uri" type="text" - ng-change="ldapProperties.ServiceAddressesUpdated = true" ng-model="ldapProperties.ServiceAddresses[0]" - required /> - <div ng-messages="ldap__configuration.ldap__uri.$error" class="form-error" - ng-class="{'visible' : ldap__configuration.ldap__uri.$touched || submitted}"> - <p ng-message="required">Field is required</p> - </div> - </div> - <div class="medium-6 large-4 columns"> - <label for="ldap__bind__dn">Bind DN</label> - <input id="ldap__bind__dn" name="ldap__bind__dn" type="text" - ng-change="ldapProperties.UsernameUpdated = true" ng-model="ldapProperties.Username" required /> - <div ng-messages="ldap__configuration.ldap__bind__dn.$error" class="form-error" - ng-class="{'visible' : ldap__configuration.ldap__bind__dn.$touched || submitted}"> - <p ng-message="required">Field is required</p> - </div> - </div> - <div class="medium-6 large-4 columns"> - <label for="ldap__bind_pw">Bind password</label> - <input id="ldap__bind_pw" type="{{showpassword ? 'text' : 'password'}}" name="ldap__bind_pw" - ng-change="ldapProperties.PasswordUpdated = true" autocomplete="off" ng-model="ldapProperties.Password" - required /> - <button ng-model="showpassword" ng-class="{'disabled' : !ldap__configuration.$valid}" - ng-click="togglePassword = !togglePassword; showpassword = !showpassword;" class="password-toggle"> - <span ng-hide="togglePassword">Show</span> - <span ng-show="togglePassword">Hide</span> - </button> - <div ng-messages="ldap__configuration.ldap__bind_pw.$error" class="form-error" - ng-class="{'visible' : ldap__configuration.ldap__bind_pw.$touched || submitted}"> - <p ng-message="required">Field is required</p> - </div> - </div> - <div class="medium-6 large-4 columns"> - <label for="ldap__base__dn">Base DN</label> - <input id="ldap__base__dn" name="ldap__base__dn" type="text" - ng-change="ldapProperties.BaseDistinguishedNamesUpdated = true" - ng-model="ldapProperties.BaseDistinguishedNames[0]" required /> - <div ng-messages="ldap__configuration.ldap__base__dn.$error" class="form-error" - ng-class="{'visible' : ldap__configuration.ldap__base__dn.$touched || submitted}"> - <p ng-message="required">Field is required</p> - </div> - </div> - <div class="medium-6 large-4 columns"> - <label for="ldap__user_attribute">User id attribute (optional)</label> - <input id="ldap__user_attribute" name="ldap__user_attribute" type="text" - ng-change="ldapProperties.UsernameAttributeUpdated = true" ng-model="ldapProperties.UsernameAttribute" - class="ldap__optional-field" /> - </div> - <div class="medium-6 large-4 columns"> - <label for="ldap__group_attribute">Group id attribute (optional)</label> - <input id="ldap__group_attribute" name="ldap__group_attribute" type="text" - ng-change="ldapProperties.GroupsAttributeUpdated = true" ng-model="ldapProperties.GroupsAttribute" - class="ldap__optional-field" /> - </div> - <div class="column ldap__configuration-buttons"> - <button type="button" class="btn btn-primary" ng-disabled="!ldap__configuration.$valid" - ng-click="$parent.submitted=true; ldap__configuration.$valid && saveLdapSettings(); ldap__configuration.$setUntouched()">Save</button> - <button type="button" class="btn btn-secondary" - ng-click="loadLdap(); ldap__configuration.$setUntouched()">Reset</button> - </div> - </fieldset> - </form> - </div> -</div> -<div class="ldap-groups row column"> - <h2 class="small-12 subhead"> - Role groups - </h2> - <div class="row column"> - <div class="small-12"> - <ldap-user-roles role-groups="roleGroups" role-group-type="roleGroupType" enabled="ldapProperties.ServiceEnabled"> - </ldap-user-roles> - </div> - </div> -</div>
\ No newline at end of file diff --git a/app/configuration/controllers/ldap-controller.js b/app/configuration/controllers/ldap-controller.js deleted file mode 100644 index 129e3db..0000000 --- a/app/configuration/controllers/ldap-controller.js +++ /dev/null @@ -1,224 +0,0 @@ -/** - * Controller for LDAP - * - * @module app/configuration - * @exports ldapController - * @name ldapController - */ - -window.angular && (function(angular) { - 'use strict'; - - angular.module('app.configuration').controller('ldapController', [ - '$scope', 'APIUtils', '$q', 'toastService', - function($scope, APIUtils, $q, toastService) { - $scope.loading = false; - $scope.isSecure = false; - $scope.ldapProperties = {}; - $scope.originalProperties = {}; - $scope.submitted = false; - $scope.roleGroups = []; - $scope.roleGroupType = ''; - $scope.clientCertificateExpires = ''; - - $scope.$on('$viewContentLoaded', function() { - $scope.loadLdap(); - }); - - $scope.loadLdap = function() { - $scope.loading = true; - $scope.submitted = false; - var getLdapProperties = - APIUtils.getAllUserAccountProperties() - .then(function(data) { - $scope.ldapProperties = { - 'ServiceEnabled': data.LDAP.ServiceEnabled ? - data.LDAP.ServiceEnabled : - data.ActiveDirectory.ServiceEnabled ? - data.ActiveDirectory.ServiceEnabled : - false, - 'LDAPServiceEnabled': data.LDAP.ServiceEnabled, - 'ADServiceEnabled': data.ActiveDirectory.ServiceEnabled, - 'EnabledServiceType': data.LDAP.ServiceEnabled ? - 'ldap' : - data.ActiveDirectory.ServiceEnabled ? 'ad' : '', - 'ServiceAddresses': data.LDAP.ServiceEnabled ? - data.LDAP.ServiceAddresses : - data.ActiveDirectory.ServiceEnabled ? - data.ActiveDirectory.ServiceAddresses : - [], - 'useSSL': $scope.isSSL( - data.LDAP.ServiceEnabled ? - data.LDAP.ServiceAddresses[0] : - data.ActiveDirectory.ServiceAddresses[0]), - 'Username': data.LDAP.ServiceEnabled ? - data.LDAP.Authentication.Username : - data.ActiveDirectory.ServiceEnabled ? - data.ActiveDirectory.Authentication.Username : - '', - 'BaseDistinguishedNames': data.LDAP.ServiceEnabled ? - data.LDAP.LDAPService.SearchSettings - .BaseDistinguishedNames : - data.ActiveDirectory.ServiceEnabled ? - data.ActiveDirectory.LDAPService.SearchSettings - .BaseDistinguishedNames : - [], - 'GroupsAttribute': data.LDAP.ServiceEnabled ? - data.LDAP.LDAPService.SearchSettings.GroupsAttribute : - data.ActiveDirectory.ServiceEnabled ? - data.ActiveDirectory.LDAPService.SearchSettings - .GroupsAttribute : - '', - 'UsernameAttribute': data.LDAP.ServiceEnabled ? - data.LDAP.LDAPService.SearchSettings.UsernameAttribute : - data.ActiveDirectory.ServiceEnabled ? - data.ActiveDirectory.LDAPService.SearchSettings - .UsernameAttribute : - '', - 'AuthenticationType': data.LDAP.ServiceEnabled ? - data.LDAP.Authentication.AuthenticationType : - data.ActiveDirectory.Authentication.AuthenticationType, - }; - - $scope.roleGroupType = - $scope.ldapProperties.EnabledServiceType; - - if ($scope.ldapProperties.ServiceEnabled) { - if ($scope.ldapProperties.LDAPServiceEnabled) { - $scope.roleGroups = data.LDAP.RemoteRoleMapping; - } else if ($scope.ldapProperties.ADServiceEnabled) { - $scope.roleGroups = - data.ActiveDirectory.RemoteRoleMapping; - } - } - }) - .catch(function(error) { - console.log(JSON.stringify(error)); - }); - var getClientCertificate = - APIUtils - .getCertificate('/redfish/v1/AccountService/LDAP/Certificates') - .then(function(data) { - if (data.Members) { - var certificate = data.Members[0]; - APIUtils.getCertificate(certificate['@odata.id']) - .then( - function(data) { - $scope.clientCertificateExpires = - data.ValidNotAfter; - }, - function(error) { - console.log(JSON.stringify(error)); - }) - } - }) - .catch(function(error) { - console.log(JSON.stringify(error)); - }); - - var promises = [getLdapProperties, getClientCertificate]; - $q.all(promises).finally(function() { - $scope.loading = false; - }); - }; - - $scope.saveLdapSettings = function() { - for (var i in $scope.ldapProperties.ServiceAddresses) { - if ($scope.ldapProperties.useSSL !== - $scope.isSSL($scope.ldapProperties.ServiceAddresses[i])) { - toastService.error( - 'Server URI ' + $scope.ldapProperties.ServiceAddresses[i] + - ' must begin with ' + - ($scope.ldapProperties.useSSL ? 'ldaps:// ' : 'ldap:// ') + - 'when SSL is ' + - ($scope.ldapProperties.useSSL ? 'configured. ' : - 'not configured.')); - } - } - - // Default LDAP and AD Attributes - let LDAP = {}; - - let ActiveDirectory = {}; - - // Data to pass to request - let data = {}; - data.LDAP = LDAP; - data.ActiveDirectory = ActiveDirectory; - - // Values to update the service type object - let Authentication = {}; - Authentication.Username = $scope.ldapProperties.Username; - Authentication.Password = $scope.ldapProperties.Password; - Authentication.AuthenticationType = - $scope.ldapProperties.AuthenticationType; - - let LDAPService = {}; - LDAPService.SearchSettings = {}; - LDAPService.SearchSettings.BaseDistinguishedNames = - $scope.ldapProperties.BaseDistinguishedNames; - LDAPService.SearchSettings.GroupsAttribute = - $scope.ldapProperties.GroupsAttribute; - LDAPService.SearchSettings.UsernameAttribute = - $scope.ldapProperties.UsernameAttribute; - - let ServiceAddresses = $scope.ldapProperties.ServiceAddresses; - if ($scope.ldapProperties.EnabledServiceType == 'ldap') { - ActiveDirectory.ServiceEnabled = false; - LDAP.ServiceEnabled = true; - LDAP.Authentication = Authentication; - LDAP.LDAPService = LDAPService; - LDAP.ServiceAddresses = ServiceAddresses; - } else if ($scope.ldapProperties.EnabledServiceType == 'ad') { - ActiveDirectory.ServiceEnabled = true; - LDAP.ServiceEnabled = false; - ActiveDirectory.Authentication = Authentication; - ActiveDirectory.LDAPService = LDAPService; - ActiveDirectory.ServiceAddresses = ServiceAddresses; - } - - APIUtils.saveLdapProperties(data).then( - function(response) { - if (!response.data.hasOwnProperty('error')) { - toastService.success('Successfully updated LDAP settings.'); - $scope.loadLdap(); - } else { - toastService.error('Unable to update LDAP settings.'); - console.log(JSON.stringify(response.data.error.message)); - } - }, - function(error) { - toastService.error('Unable to update LDAP settings.'); - console.log(JSON.stringify(error)); - }); - }; - - $scope.isSSL = function(uri) { - return uri.startsWith('ldaps://'); - }; - $scope.updateServiceEnabled = function() { - if (!$scope.ldapProperties.ServiceEnabled) { - $scope.ldapProperties.EnabledServiceType = ''; - let data = {}; - let LDAP = {}; - data.LDAP = LDAP; - LDAP.ServiceEnabled = false; - let ActiveDirectory = {}; - data.ActiveDirectory = ActiveDirectory; - ActiveDirectory.ServiceEnabled = false; - - APIUtils.saveLdapProperties(data).then( - function(response) { - toastService.success('Successfully disabled LDAP.'); - $scope.roleGroups = []; - $scope.loadLdap(); - }, - function(error) { - toastService.error('Unable to disable LDAP.'); - console.log(JSON.stringify(error)); - }); - } - } - } - ]); -})(angular); diff --git a/app/configuration/index.js b/app/configuration/index.js index 4ad7ff9..b418295 100644 --- a/app/configuration/index.js +++ b/app/configuration/index.js @@ -35,17 +35,6 @@ window.angular && (function(angular) { 'controller': 'snmpController', authenticated: true }) - .when('/configuration/certificate', { - 'template': - require('./controllers/certificate-controller.html'), - 'controller': 'certificateController', - authenticated: true - }) - .when('/configuration/ldap', { - 'template': require('./controllers/ldap-controller.html'), - 'controller': 'ldapController', - authenticated: true - }) .when('/configuration/firmware', { 'template': require('./controllers/firmware-controller.html'), 'controller': 'firmwareController', diff --git a/app/configuration/styles/certificate.scss b/app/configuration/styles/certificate.scss deleted file mode 100644 index a7c57f2..0000000 --- a/app/configuration/styles/certificate.scss +++ /dev/null @@ -1,250 +0,0 @@ -.certificate__table { - border-left: 1px solid $border-color-01; - border-right: 1px solid $border-color-01; - margin-top: 0.5em; - .table__row-header { - width: 100%; - border-bottom: 1px solid $border-color-01; - background-color: $primary-dark; - color: $primary-light; - font-weight: 700; - } - .table__row-value { - width: 100%; - border-bottom: 1px solid $border-color-01; - } - .certificate__type-header { - @include mediaQuery(small) { - width: 20%; - background: transparent; - } - width: 100%; - padding: 0.8em; - padding-left: 1.5em; - } - .certificate__issue-header { - display: none; - padding: 0.8em; - @include mediaQuery(small) { - width: 20%; - display: block; - } - } - .certificate__date-header { - padding: 0.8em; - @include mediaQuery(small) { - width: 12%; - display: block; - } - display: none; - } - .certificate__status-header { - padding: 0.8em; - @include mediaQuery(small) { - width: 5%; - display: block; - } - display: none; - } - .certificate__type-cell { - width: 100%; - padding: 0.8em 0.8em 0.8em 1.5em; - word-wrap: break-word; - background: $background-02; - @include mediaQuery(small) { - width: 20%; - background: transparent; - } - } - .certificate__issue-cell { - padding: 0.8em; - word-wrap: break-word; - @include mediaQuery(small) { - width: 20%; - } - width: 70%; - } - .certificate__date-cell { - width: 70%; - padding: 0.8em; - word-wrap: break-word; - @include mediaQuery(small) { - width: 12%; - } - } - .certificate__status-cell { - padding: 0.8em; - @include mediaQuery(small) { - display: block; - width: 5%; - } - display: none; - } - .certificate__status-icon { - width: 1.2em; - svg { - margin-bottom: .2em; - } - } - .certificate__buttons-cell { - @include mediaQuery(small) { - width: 10%; - padding-top: 0.5em; - } - width: 100%; - text-align: right; - padding: 0 1.5em 0 0; - } - .certificate__title-inline { - @include mediaQuery(small) { - display: none; - } - width: 30%; - display: block; - padding: 0.8em 0.8em 0.8em 1.5em; - } - .upload__certificate { - border-top: 1px solid $border-color-01; - width: 100%; - background: $background-02; - padding: 0.8em; - } -} -.certificate__upload-chooser { - background: $background-02; -} - - -.certificate__close-modal { - float: right; - position: relative; - bottom: 1rem; - left: 2rem; -} -.certificate__table__icon { - margin-left: 1.5em; - margin-bottom: .4em; -} - -.add__certificate__modal { - select { - margin-bottom: 0; - } - .file__upload { - margin-bottom: 2em; - } - .select__new-label { - margin-bottom: 1em; - } - .select__new-button { - font-size: 1.2em; - } - .file__name { - background-color: $background-02; - padding: 0.5em; - } -} - -.add-certificate__section { - padding-left: 0; -} - -// Combinator needed to match specificity -// of default modal settings -.modal.add-csr__modal { - width: 100%; - max-height: 100vh; - overflow-y: auto; - z-index: 1001; -} - -.add-csr__section:first-of-type { - padding-left: 0; -} - -.add-csr__section:last-of-type { - margin-top: 2rem; - padding-right: 0; - - @media (min-width: 640px) { - margin-top: 0; - } -} - -.add-csr__section-title { - margin-bottom: 1rem; - font-weight: 700; -} - -.add-csr__section--border { - @media (min-width: 640px) { - padding-left: 2rem; - border-left: 1px solid $base-02--04; - } -} - -.add-csr__label { - white-space: nowrap; - display: inline-block; -} - -.add-csr__text-helper { - color: $base-02--02; - font-weight: 400; - font-size: 14px; - line-height: 1.2; - margin-bottom: .5em; -} - -input.add-csr__input, -select.add-csr__select { - width: 100%; - margin-bottom: 0; - max-height: none; - height: auto; -} - -.select.add-csr__select { - line-height: 1.15; -} - -input.add-csr__input-no-validation { - margin-bottom: 1.7rem; -} - -.add-csr__additional-alt-names { - display: flex; -} - -.add-csr__alt-name-add-btn { - padding: 0; - @media (min-width: 640px) { - margin: 1.9rem 0; - } -} - -.add-csr__alt-name-delete-btn { - width: 20px; - height: 30px; - padding: 0; - - icon { - margin-right: 0; - } -} - -.add-csr-code__header { - margin-top: 1em; -} - -.add-csr__container-csr-code { - white-space: pre-wrap; -} - -.add-csr__text-download { - color: $base-02--08; -} - -select.add-csr__multiselect { - height: 14rem; -} diff --git a/app/configuration/styles/index.scss b/app/configuration/styles/index.scss index 2e70e84..e532583 100644 --- a/app/configuration/styles/index.scss +++ b/app/configuration/styles/index.scss @@ -2,5 +2,3 @@ @import "./snmp.scss"; @import "./date-time.scss"; @import "./firmware.scss"; -@import "./certificate.scss"; -@import "./ldap.scss"; diff --git a/app/configuration/styles/ldap.scss b/app/configuration/styles/ldap.scss deleted file mode 100644 index a18ac70..0000000 --- a/app/configuration/styles/ldap.scss +++ /dev/null @@ -1,269 +0,0 @@ -// LDAP SCSS - -.ldap__optional-field { - margin-bottom: 1.7em; -} - -.ldap__configure-settings { - background-color: $base-02--06; - padding-top: 1.5em; - padding-bottom: 1.5em; - margin-top: 1em; - margin-bottom: 3em; -} - -.ldap__server-info { - @media (min-width: 1024px) { - border-left: 1px solid $border-color-01; - } - - .control-radio { - margin-bottom: 6px; - display: block; - } - - .service-type-column { - margin-bottom: 1.2em; - } -} - -.ldap__ssl-column { - padding-left: 1.5em; - .control__label { - text-transform: none; - font-weight: 400; - font-size: 16px; - color: $primary-dark; - } - .control__indicator { - top: 5px; - } -} - -.ldap__configuration-buttons { - margin-top: 1rem; - - @media (min-width: 1024px) { - margin-top: 0; - } - - .btn { - float: right; - margin-left: 0.5em; - margin-top: 0.5em; - } - - .btn-secondary { - background-color: $primary-light; - } - - .btn-secondary:disabled { - color: $base-02--03; - border-color: $border-color-02; - } -} - -.ldap__server-info-service-type { - .content-label { - margin-bottom: 1rem; - } -} - -.ldap__certificate-info { - padding-top: 0.5em; - small { - font-size: 14px; - } - p { - color: $base-02--02; - text-transform: uppercase; - font-weight: 700; - font-size: 0.75em; - margin-bottom: 0; - } -} - -.control__radio__label { - padding: 0.2em 1em 0 2em; - text-transform: none; - font-weight: 400; - font-size: 16px; - color: $primary-dark; -} - -.ldap__control-check { - text-transform: none; - font-weight: 400; - font-size: 16px; - color: $primary-dark; - - .control__indicator { - top: 11px; - } - - .control__label { - margin-left: 30px; - } -} - -.control-radio .control__indicator-service-type { - width: 20px; - height: 20px; -} - -.control-radio .control__indicator-service-type:after { - top: 3px; - left: 3px; - width: 10px; - height: 10px; -} - -.control-radio input:disabled ~ .control__indicator-service-type:after { - top: 0; - left: 0; - width: 20px; - height: 20px; -} - -.password-toggle { - color: $base-01--03; - font-size: 0.8em; - float: right; - position: relative; - z-index: 2; - padding: 6px 0 0 0; -} - -.password-toggle.disabled { - background: transparent; - color: $base-02--03; - border: none; -} - -.ldap-groups { - .ldap__table { - border-left: 1px solid $border-color-01; - border-right: 1px solid $border-color-01; - - .empty__logs { - margin-top: 0; - } - - .table__row-header { - width: 100%; - border-bottom: 1px solid $border-color-01; - background-color: $primary-dark; - color: $primary-light; - font-weight: 700; - padding: 0; - } - - .table__row-header.disabled { - opacity: 0.8; - } - - .table__row-value { - width: 100%; - border-bottom: 1px solid $border-color-01; - } - - .table__cell-ldap { - width: 30%; - padding: 1.3em 1.5em 0.8em 1.5em; - } - - .table__cell-select { - width: 8%; - padding: 1.3em 1.5em 0.8em 1.5em; - .select-header { - padding-top: 1em; - } - } - - .table__cell-sort { - padding: 0.4em 1em 0 0; - margin: 0 -25px 0 -13px; - } - - .table__cell-ldap-group { - width: 29%; - padding: 1.2em 0.5em 0.8em 0.75em; - } - - .table__cell-ldap-role { - width: 30%; - padding: 1.2em 0.5em 0.8em 0.75em; - } - - .table__cell-buttons { - width: 32%; - text-align: right; - padding: 0.8em; - .btn { - padding-left: 0; - padding-right: 0; - } - } - } - - .btn-add-group, - .btn-remove-group { - color: $base-01--03; - padding: 0.75em 0; - } - - .modal__content-ldap { - margin-bottom: 2em; - margin-top: 2em; - input[type="text"] { - max-height: 2.4em; - } - select { - margin: 0 0 0; - } - } - - .edit-group-name { - padding-bottom: 1em; - } - - .form-actions { - width: 100%; - padding-top: 2em; - margin-top: 1.5em; - border-top: 1px solid $border-color-01; - button { - display: block; - float: right; - margin: 0 0 0 1em; - } - } - - .sort-ascending, - .sort-descending { - display: block; - padding: 0; - color: $primary-light; - font-size: 1em; - transform: rotate(-90deg); - - &:hover { - color: $primary-accent; - } - - &:after { - content: "\276F"; - } - - &:focus { - outline: 0; - color: $primary-accent; - } - } - - .sort-descending { - &:after { - content: "\276e"; - } - } -} |

