├── client ├── version │ └── .gitkeep ├── app │ ├── layouts │ │ ├── blank.html │ │ ├── _layouts.sass │ │ └── _navigation.sass │ ├── states │ │ ├── 404 │ │ │ ├── 404.html │ │ │ ├── 404.state.js │ │ │ ├── 404.state.spec.js │ │ │ └── _404.sass │ │ ├── help │ │ │ ├── help.html │ │ │ └── help.state.js │ │ ├── about-me │ │ │ ├── about-me.html │ │ │ └── about-me.state.js │ │ ├── catalogs │ │ │ ├── explorer │ │ │ │ ├── catalogs.html │ │ │ │ └── explorer.state.js │ │ │ ├── details │ │ │ │ └── _details.sass │ │ │ ├── catalogs.state.js │ │ │ └── catalogs.state.spec.js │ │ ├── services │ │ │ ├── explorer │ │ │ │ ├── explorer.html │ │ │ │ └── explorer.state.js │ │ │ ├── details │ │ │ │ ├── details.html │ │ │ │ ├── details.state.js │ │ │ │ └── details.state.spec.js │ │ │ ├── services.state.js │ │ │ ├── resource-details │ │ │ │ └── details.state.js │ │ │ └── custom_button_details │ │ │ │ └── custom_button_details.html │ │ ├── orders │ │ │ ├── details │ │ │ │ ├── _details.sass │ │ │ │ ├── details.state.spec.js │ │ │ │ ├── details.state.js │ │ │ │ └── details.html │ │ │ ├── orders.state.js │ │ │ └── explorer │ │ │ │ └── explorer.state.js │ │ ├── states.module.spec.js │ │ ├── oidc │ │ │ └── oidc-login.state.js │ │ ├── _states.sass │ │ ├── vms │ │ │ ├── vms.state.js │ │ │ └── snapshots │ │ │ │ ├── snapshots.state.js │ │ │ │ └── snapshots.state.spec.js │ │ ├── dashboard │ │ │ ├── dashboard.state.spec.js │ │ │ └── dashboard.state.js │ │ ├── error │ │ │ ├── error.state.spec.js │ │ │ ├── error.html │ │ │ ├── error.state.js │ │ │ └── _error.sass │ │ ├── login │ │ │ ├── _login.sass │ │ │ └── login.state.spec.js │ │ └── logout │ │ │ └── logout.state.js │ ├── shared │ │ ├── timeline │ │ │ ├── _timeline.sass │ │ │ └── timeline.component.spec.js │ │ ├── custom-dropdown │ │ │ ├── _custom-dropdown.sass │ │ │ ├── custom-dropdown.component.js │ │ │ ├── custom-dropdown.component.spec.js │ │ │ └── custom-dropdown.html │ │ ├── icon-list │ │ │ ├── icon-list.component.js │ │ │ ├── _icon-list.sass │ │ │ └── icon-list.html │ │ ├── substitute.filter.js │ │ ├── ss-card │ │ │ ├── ss-card.component.js │ │ │ ├── ss-card.html │ │ │ └── _ss-card.sass │ │ ├── loading.component.js │ │ ├── autofocus.directive.js │ │ ├── language-switcher │ │ │ ├── _language-switcher.sass │ │ │ ├── language-switcher.html │ │ │ ├── language-switcher.component.js │ │ │ └── language-switcher.component.spec.js │ │ ├── pagination │ │ │ ├── _pagination.sass │ │ │ └── pagination.component.spec.js │ │ ├── substitute.filter.spec.js │ │ ├── format-bytes.filter.js │ │ ├── elapsedTime.filter.spec.js │ │ ├── elapsedTime.filter.js │ │ ├── tagging │ │ │ ├── _tagging.sass │ │ │ └── tagging.html │ │ ├── confirmation │ │ │ ├── confirmation.component.spec.js │ │ │ ├── _confirmation.sass │ │ │ └── confirmation.html │ │ ├── format-bytes.filter.spec.js │ │ └── action-button-group │ │ │ └── action-button-group.html │ ├── core │ │ ├── shopping-cart │ │ │ ├── _shopping-cart.sass │ │ │ └── shopping-cart.component.js │ │ ├── router │ │ │ └── router.module.js │ │ ├── core.module.spec.js │ │ ├── config.js │ │ ├── exception │ │ │ ├── exception.module.js │ │ │ └── exception.service.js │ │ ├── modal │ │ │ ├── base-modal.factory.js │ │ │ ├── base-modal.factory.spec.js │ │ │ ├── base-modal-controller.js │ │ │ └── base-modal-controller.spec.js │ │ ├── polling.service.spec.js │ │ ├── layouts.config.js │ │ ├── site-switcher │ │ │ ├── site-switcher.html │ │ │ ├── site-switcher.component.spec.js │ │ │ ├── site-switcher.component.js │ │ │ └── _site-switcher.sass │ │ ├── list-configuration.service.js │ │ ├── navigation.service.spec.js │ │ ├── polling.service.js │ │ ├── gettext.config.js │ │ ├── tag-editor-modal │ │ │ ├── tag-editor-modal.html │ │ │ └── tag-editor-modal.service.js │ │ ├── appliance-info.service.js │ │ ├── save-modal-dialog │ │ │ ├── save-modal-dialog.html │ │ │ └── save-modal-dialog.factory.js │ │ ├── rbac.service.js │ │ ├── rbac.service.spec.js │ │ ├── list-view.service.js │ │ └── dialog-field-refresh.service.js │ ├── app.module.spec.js │ ├── components │ │ ├── notifications │ │ │ ├── subheading.html │ │ │ ├── heading.html │ │ │ ├── _notifications.sass │ │ │ └── notification-footer.html │ │ ├── _components.sass │ │ ├── components.module.js │ │ └── dashboard │ │ │ ├── dashboard.component.spec.js │ │ │ └── dashboard.component.service.spec.js │ ├── services │ │ ├── custom-button │ │ │ ├── _custom-button-menu.sass │ │ │ └── _custom-button.sass │ │ ├── generic-objects-list │ │ │ ├── _generic_objects_list.sass │ │ │ └── generic-objects-list.component.js │ │ ├── vms │ │ │ └── _snapshots.sass │ │ ├── service-details │ │ │ ├── service-details-ansible-modal.html │ │ │ ├── service-details-ansible-modal.component.js │ │ │ └── _service-details.sass │ │ ├── usage-graphs │ │ │ ├── _usage-graphs.sass │ │ │ ├── usage-graphs.service.js │ │ │ ├── usage-graphs.service.spec.js │ │ │ └── usage-graphs.component.js │ │ ├── resource-details │ │ │ └── _resource-details.sass │ │ ├── process-snapshots-modal │ │ │ ├── process-snapshots-modal.component.spec.js │ │ │ └── process-snapshots-modal.html │ │ ├── detail-reveal │ │ │ ├── detail-reveal.component.js │ │ │ ├── detail-reveal.html │ │ │ └── _detail-reveal.sass │ │ ├── edit-service-modal │ │ │ ├── edit-service-modal.component.js │ │ │ └── edit-service-modal.html │ │ ├── retire-remove-service-modal │ │ │ ├── retire-remove-service-modal.html │ │ │ └── retire-remove-service-modal.component.js │ │ └── retire-service-modal │ │ │ ├── retire-service-modal.spec.js │ │ │ └── retire-service-modal.html │ ├── orders │ │ ├── request-list │ │ │ ├── requests-list.component.js │ │ │ └── requests-list.component.spec.js │ │ ├── process-order-modal │ │ │ ├── process-order-modal.component.spec.js │ │ │ ├── process-order-modal.html │ │ │ └── process-order-modal.component.js │ │ ├── orders.module.js │ │ └── order-explorer │ │ │ └── order-explorer.component.spec.js │ ├── globals.js │ ├── catalogs │ │ ├── catalogs.module.js │ │ └── catalog-explorer.html │ ├── skin │ │ └── skin.module.js │ ├── app.module.js │ └── app.controller.js ├── assets │ ├── sass │ │ ├── _buttons.sass │ │ ├── _bem-support │ │ │ ├── _index.sass │ │ │ ├── _functions.scss │ │ │ └── _mixins.scss │ │ ├── _base.sass │ │ ├── breadcrumbs.sass │ │ ├── _patternfly.sass │ │ ├── _functions.sass │ │ ├── _splash.sass │ │ ├── styles.sass │ │ ├── _explorer.sass │ │ └── _overrides.sass │ └── images │ │ ├── cockpit.png │ │ ├── navbar.png │ │ ├── service.png │ │ ├── tenant.png │ │ ├── bg-login.png │ │ ├── bg-navbar.png │ │ ├── quadicon.png │ │ ├── bg-login-2.png │ │ ├── bg-modal-about-pf.png │ │ ├── brand_transparent.png │ │ ├── quadicon-single.png │ │ ├── service_template.png │ │ ├── os │ │ ├── os-linux_ubuntu.png │ │ ├── os-windows_generic.svg │ │ ├── os-linux_esx.svg │ │ ├── os-esx-server-3i.svg │ │ ├── os-vmware-esx-server.svg │ │ ├── os-linux_coreos.svg │ │ └── os-linux_chrome.svg │ │ └── providers │ │ ├── vendor-scvmm.svg │ │ ├── vendor-hyper-v.svg │ │ ├── vendor-microsoft.svg │ │ ├── vendor-parallels.svg │ │ ├── vendor-ansible_tower_configuration.svg │ │ ├── vendor-xen.svg │ │ ├── vendor-xensource.svg │ │ ├── vendor-citrix.svg │ │ ├── vendor-azure.svg │ │ ├── vendor-azure_network.svg │ │ ├── vendor-vmware.svg │ │ ├── vendor-vmwarews.svg │ │ ├── vendor-vmware_cloud.svg │ │ ├── vendor-vmware_cloud_network.svg │ │ ├── vendor-openshift.svg │ │ ├── vendor-openshift_enterprise.svg │ │ ├── vendor-amazon.svg │ │ ├── vendor-ec2.svg │ │ ├── vendor-ec2_network.svg │ │ └── vendor-jboss-eap.svg ├── gettext │ └── json │ │ └── available_languages.json ├── console │ ├── common.js │ └── common.ejs └── index.ejs ├── .eslintignore ├── skin-sample ├── images │ ├── bg-login.png │ ├── bg-navbar.png │ ├── bg-login-2.png │ ├── bg-modal-about-pf.png │ └── nothing.png ├── skin.css ├── unlink.sh ├── skin.js └── link.sh ├── bin ├── setup └── before_install ├── .babelrc ├── .whitesource ├── postcss.config.js ├── tests ├── mock │ ├── poweroperations │ │ ├── vm_retire.json │ │ ├── vm_retire_failure.json │ │ ├── stop.json │ │ ├── vm_stop.json │ │ ├── vm_start.json │ │ ├── vm_suspend.json │ │ ├── vm_start_failure.json │ │ ├── service_start.json │ │ ├── service_stop.json │ │ ├── service_suspend.json │ │ ├── service_start_failure.json │ │ └── service_suspend_failure.json │ ├── authentication-api │ │ ├── failure.json │ │ └── success.json │ └── services │ │ ├── vmPermissions.json │ │ ├── service1_tags.json │ │ ├── serviceDetailsAnsibleComponent.json │ │ └── servicePermissions.json └── utils │ └── utils.spec.js ├── renovate.json ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ ├── update_yarn_lock.yaml │ └── ci.yaml ├── .tx └── config ├── .gitignore ├── language ├── validate.js └── validate-language-codes.js ├── config ├── webpack.testing.js ├── githash.js ├── available-languages.js └── manifest.js ├── Rakefile ├── docs ├── troubleshooting.md └── contributing.md ├── .eslintrc.js └── .yarnrc.yml /client/version/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /reports/ 2 | -------------------------------------------------------------------------------- /skin-sample/images/bg-login.png: -------------------------------------------------------------------------------- 1 | nothing.png -------------------------------------------------------------------------------- /skin-sample/images/bg-navbar.png: -------------------------------------------------------------------------------- 1 | nothing.png -------------------------------------------------------------------------------- /skin-sample/images/bg-login-2.png: -------------------------------------------------------------------------------- 1 | nothing.png -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -vx 4 | yarn 5 | -------------------------------------------------------------------------------- /client/app/layouts/blank.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /skin-sample/images/bg-modal-about-pf.png: -------------------------------------------------------------------------------- 1 | nothing.png -------------------------------------------------------------------------------- /client/app/states/help/help.html: -------------------------------------------------------------------------------- 1 | {{'Help'|translate}} 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /client/app/states/about-me/about-me.html: -------------------------------------------------------------------------------- 1 | {{'About Me'|translate}} 2 | -------------------------------------------------------------------------------- /client/app/shared/timeline/_timeline.sass: -------------------------------------------------------------------------------- 1 | .timeline 2 | position: relative 3 | -------------------------------------------------------------------------------- /client/assets/sass/_buttons.sass: -------------------------------------------------------------------------------- 1 | a 2 | &.dropdown-toggle 3 | cursor: pointer 4 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "settingsInheritedFrom": "ManageIQ/whitesource-config@master" 3 | } 4 | -------------------------------------------------------------------------------- /client/app/states/catalogs/explorer/catalogs.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /client/app/states/services/explorer/explorer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /client/app/core/shopping-cart/_shopping-cart.sass: -------------------------------------------------------------------------------- 1 | .cart-duplicate-icon 2 | padding: 0 .5em 3 | -------------------------------------------------------------------------------- /client/app/states/services/details/details.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /client/assets/images/cockpit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-ui-service/HEAD/client/assets/images/cockpit.png -------------------------------------------------------------------------------- /client/assets/images/navbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-ui-service/HEAD/client/assets/images/navbar.png -------------------------------------------------------------------------------- /client/assets/images/service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-ui-service/HEAD/client/assets/images/service.png -------------------------------------------------------------------------------- /client/assets/images/tenant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-ui-service/HEAD/client/assets/images/tenant.png -------------------------------------------------------------------------------- /skin-sample/images/nothing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-ui-service/HEAD/skin-sample/images/nothing.png -------------------------------------------------------------------------------- /client/assets/images/bg-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-ui-service/HEAD/client/assets/images/bg-login.png -------------------------------------------------------------------------------- /client/assets/images/bg-navbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-ui-service/HEAD/client/assets/images/bg-navbar.png -------------------------------------------------------------------------------- /client/assets/images/quadicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-ui-service/HEAD/client/assets/images/quadicon.png -------------------------------------------------------------------------------- /client/assets/images/bg-login-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-ui-service/HEAD/client/assets/images/bg-login-2.png -------------------------------------------------------------------------------- /client/assets/images/bg-modal-about-pf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-ui-service/HEAD/client/assets/images/bg-modal-about-pf.png -------------------------------------------------------------------------------- /client/assets/images/brand_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-ui-service/HEAD/client/assets/images/brand_transparent.png -------------------------------------------------------------------------------- /client/assets/images/quadicon-single.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-ui-service/HEAD/client/assets/images/quadicon-single.png -------------------------------------------------------------------------------- /client/assets/images/service_template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-ui-service/HEAD/client/assets/images/service_template.png -------------------------------------------------------------------------------- /client/app/app.module.spec.js: -------------------------------------------------------------------------------- 1 | describe('app', () => { 2 | it('passes the smoke test', () => { 3 | expect(true).to.eql(true) 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /client/app/components/notifications/subheading.html: -------------------------------------------------------------------------------- 1 | {{notificationGroup.unreadCount}} {{'New' | translate}} 2 | -------------------------------------------------------------------------------- /client/app/shared/custom-dropdown/_custom-dropdown.sass: -------------------------------------------------------------------------------- 1 | .custom-dropdown 2 | margin-right: 5px 3 | 4 | &:last-of-type 5 | margin-right: 0 6 | -------------------------------------------------------------------------------- /client/assets/images/os/os-linux_ubuntu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManageIQ/manageiq-ui-service/HEAD/client/assets/images/os/os-linux_ubuntu.png -------------------------------------------------------------------------------- /client/assets/sass/_bem-support/_index.sass: -------------------------------------------------------------------------------- 1 | 2 | $element-separator: '__' 3 | $modifier-separator: '--' 4 | @import 'functions' 5 | @import 'mixins' 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | module.exports = { 4 | plugins: [ 5 | require('autoprefixer')() 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tests/mock/poweroperations/vm_retire.json: -------------------------------------------------------------------------------- 1 | {"success":true,"message":"Successfully retired VM","href":"http://localhost:3001/api/vms/10000000002056"} 2 | -------------------------------------------------------------------------------- /tests/mock/poweroperations/vm_retire_failure.json: -------------------------------------------------------------------------------- 1 | {"success":false,"message":"Failed to retire VM","href":"http://localhost:3001/api/vms/10000000002056"} 2 | -------------------------------------------------------------------------------- /client/assets/sass/_base.sass: -------------------------------------------------------------------------------- 1 | body 2 | background-color: $app-color-light-gray-3 3 | 4 | h2 5 | span 6 | &.detail 7 | float: right 8 | font-size: 14px 9 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "inheritConfig": true, 4 | "inheritConfigRepoName": "manageiq/renovate-config" 5 | } 6 | -------------------------------------------------------------------------------- /client/app/services/custom-button/_custom-button-menu.sass: -------------------------------------------------------------------------------- 1 | .custom-button-menu 2 | margin-left: 5px 3 | padding-left: 6px 4 | width: 25px 5 | 6 | .btn-link 7 | margin-bottom: 4px 8 | -------------------------------------------------------------------------------- /client/app/components/notifications/heading.html: -------------------------------------------------------------------------------- 1 | {{notificationGroup.heading}} 2 | {{notificationGroup.subHeading}} 3 | 4 | -------------------------------------------------------------------------------- /client/app/layouts/_layouts.sass: -------------------------------------------------------------------------------- 1 | // This file should be @imported into client/assets/sass/styles.sass 2 | // 3 | // @import all of your layout styles here 4 | 5 | @import 'application' 6 | @import 'navigation' 7 | -------------------------------------------------------------------------------- /skin-sample/skin.css: -------------------------------------------------------------------------------- 1 | // splash screen 2 | .splash-message:before { 3 | content: 'Magic UI'; 4 | } 5 | 6 | .login-pf, 7 | .navbar-pf-vertical, 8 | .about-modal-pf { 9 | background-color: #404; 10 | } 11 | -------------------------------------------------------------------------------- /tests/mock/authentication-api/failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "kind": "unauthorized", 4 | "message": "Authentication failed", 5 | "klass": "MiqException::MiqEVMLoginError" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/app/states/orders/details/_details.sass: -------------------------------------------------------------------------------- 1 | .list-view-container 2 | &.order-details 3 | height: calc(100vh - 272px) 4 | 5 | .ss-details-wrapper 6 | .order-details 7 | .list-view-pf 8 | margin-top: 0 9 | -------------------------------------------------------------------------------- /tests/mock/authentication-api/success.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "auth_token": "81e0fa13cde428f3d0f94a76960abc7f", 4 | "token_ttl": 3600, 5 | "expires_on": "2019-01-12T22:21:24Z" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/app/services/generic-objects-list/_generic_objects_list.sass: -------------------------------------------------------------------------------- 1 | .generic-objects-container 2 | .generic-objects-title 3 | cursor: pointer 4 | 5 | .generic-object-icon 6 | max-height: 30px 7 | max-width: 30px 8 | -------------------------------------------------------------------------------- /client/gettext/json/available_languages.json: -------------------------------------------------------------------------------- 1 | {"de":"Deutsch","es":"Español","fr":"Français","it":"Italiano","ja":"日本語","ko":"한국어","nl":"Nederlands","pt-BR":"Português (Brasil)","pt-PT":"Português (Portugal)","zh-CN":"简体中文","zh-TW":"繁體中文"} -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Description of problem: 2 | 3 | 4 | How reproducible: 5 | 6 | 7 | Steps to Reproduce: 8 | 1. 9 | 2. 10 | 3. 11 | 12 | 13 | Actual results: 14 | 15 | 16 | Expected results: 17 | 18 | 19 | Additional info: -------------------------------------------------------------------------------- /client/app/components/_components.sass: -------------------------------------------------------------------------------- 1 | // This file should be @imported into client/assets/sass/styles.sass 2 | // 3 | // @import all of your component styles here 4 | 5 | @import 'notifications/notifications' 6 | @import './dashboard/dashboard' 7 | -------------------------------------------------------------------------------- /client/app/core/router/router.module.js: -------------------------------------------------------------------------------- 1 | import { routerHelperProvider } from './router-helper.provider.js' 2 | 3 | export const RouterModule = angular 4 | .module('app.core.router', []) 5 | .provider('routerHelper', routerHelperProvider) 6 | .name 7 | -------------------------------------------------------------------------------- /client/assets/sass/breadcrumbs.sass: -------------------------------------------------------------------------------- 1 | .breadcrumb-bar 2 | display: flex 3 | 4 | .breadcrumb-actions 5 | margin-left: auto 6 | padding: 5px 15px 7 | 8 | .breadcrumb-content 9 | height: calc(100vh - 94px) 10 | overflow-y: auto 11 | -------------------------------------------------------------------------------- /tests/mock/poweroperations/stop.json: -------------------------------------------------------------------------------- 1 | {"success":true,"message":"VM id:10000000002056 name:'niickapp' stopping","task_id":10000000194021,"task_href":"http://localhost:3001/api/tasks/10000000194021","href":"http://localhost:3001/api/vms/10000000002056"} 2 | -------------------------------------------------------------------------------- /client/app/shared/icon-list/icon-list.component.js: -------------------------------------------------------------------------------- 1 | import './_icon-list.sass' 2 | import template from './icon-list.html'; 3 | 4 | export const IconListComponent = { 5 | controllerAs: 'vm', 6 | bindings: { 7 | items: '<' 8 | }, 9 | template, 10 | } 11 | -------------------------------------------------------------------------------- /client/app/states/catalogs/details/_details.sass: -------------------------------------------------------------------------------- 1 | +block(ss-marketplace-details-header) 2 | border-bottom: 0 3 | 4 | .ss-marketplace-details-form 5 | h1 6 | margin-bottom: 20px 7 | 8 | .dialog-box 9 | display: block 10 | position: initial 11 | width: 80% 12 | -------------------------------------------------------------------------------- /client/app/orders/request-list/requests-list.component.js: -------------------------------------------------------------------------------- 1 | import template from './requests-list.html'; 2 | 3 | export const RequestsListComponent = { 4 | bindings: { 5 | 'items': '<', 6 | 'config': '/manageiq-ui-service.po 6 | source_file = client/gettext/po/manageiq-ui-service.pot 7 | source_lang = en 8 | type = PO 9 | 10 | -------------------------------------------------------------------------------- /client/app/states/states.module.spec.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict' 3 | 4 | angular.module('app.states') 5 | .run(mock) 6 | 7 | function mock ($httpBackend) { 8 | $httpBackend.when('GET', /available_languages.json/) 9 | .respond({}) 10 | } 11 | })() 12 | -------------------------------------------------------------------------------- /client/app/services/vms/_snapshots.sass: -------------------------------------------------------------------------------- 1 | @import 'app_colors' 2 | 3 | div 4 | .timeline 5 | border-bottom: 1px solid $app-color-light-gray-5 6 | margin-left: -120px 7 | 8 | .timeline-pf-label 9 | display: none 10 | 11 | .well-panel 12 | background-color: $app-color-white 13 | -------------------------------------------------------------------------------- /tests/mock/poweroperations/vm_stop.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "message": "VM id:10000000002056 name:'niickapp' stopping", 4 | "task_id": 10000000194024, 5 | "task_href": "http://localhost:3001/api/tasks/10000000194024", 6 | "href": "http://localhost:3001/api/vms/10000000002056" 7 | } 8 | -------------------------------------------------------------------------------- /tests/mock/poweroperations/vm_start.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "message": "VM id:10000000002056 name:'niickapp' starting", 4 | "task_id": 10000000194024, 5 | "task_href": "http://localhost:3001/api/tasks/10000000194024", 6 | "href": "http://localhost:3001/api/vms/10000000002056" 7 | } 8 | -------------------------------------------------------------------------------- /tests/mock/poweroperations/vm_suspend.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "message": "VM id:10000000002056 name:'niickapp' suspending", 4 | "task_id": 10000000194024, 5 | "task_href": "http://localhost:3001/api/tasks/10000000194024", 6 | "href": "http://localhost:3001/api/vms/10000000002056" 7 | } 8 | -------------------------------------------------------------------------------- /tests/mock/poweroperations/vm_start_failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": false, 3 | "message": "VM id:10000000002056 name:'niickapp' failed", 4 | "task_id": 10000000194024, 5 | "task_href": "http://localhost:3001/api/tasks/10000000194024", 6 | "href": "http://localhost:3001/api/vms/10000000002056" 7 | } 8 | -------------------------------------------------------------------------------- /client/app/shared/substitute.filter.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function substitute ($interpolate) { 3 | return function (text, context) { 4 | text = text.replace(/\[\[/g, '{{').replace(/\]\]/g, '}}') 5 | var interpolateFn = $interpolate(text) 6 | 7 | return interpolateFn(context) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/mock/services/vmPermissions.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": false, 3 | "stop": true, 4 | "suspend": true, 5 | "tags": true, 6 | "snapshotsView": true, 7 | "snapshotsAdd": true, 8 | "snapshotsDelete": true, 9 | "deleteAll": true, 10 | "revert": true, 11 | "retire": true 12 | } 13 | -------------------------------------------------------------------------------- /client/app/shared/ss-card/ss-card.component.js: -------------------------------------------------------------------------------- 1 | import './_ss-card.sass' 2 | import template from './ss-card.html'; 3 | 4 | export const SSCardComponent = { 5 | bindings: { 6 | header: '<', 7 | subHeader: '<', 8 | description: '<', 9 | image: '<' 10 | }, 11 | controllerAs: 'vm', 12 | template, 13 | } 14 | -------------------------------------------------------------------------------- /client/app/core/core.module.spec.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict' 3 | 4 | angular.module('app.core') 5 | .run(mock) 6 | 7 | function mock ($httpBackend) { 8 | $httpBackend.when('GET', /available_languages.json/) 9 | .respond({}) 10 | $httpBackend.when('GET', /\/api\/notifications/) 11 | .respond({}) 12 | } 13 | })() 14 | -------------------------------------------------------------------------------- /client/app/services/service-details/service-details-ansible-modal.html: -------------------------------------------------------------------------------- 1 | 7 | 9 | -------------------------------------------------------------------------------- /tests/mock/poweroperations/service_start.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "message": "Service id:10000000000619 name:'Deploy Ticket Monster on VMware-20170222-115347' starting", 4 | "task_id": 10000000194022, 5 | "task_href": "http://localhost:3001/api/tasks/10000000194022", 6 | "href": "http://localhost:3001/api/services/10000000000619" 7 | } 8 | -------------------------------------------------------------------------------- /tests/mock/poweroperations/service_stop.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "message": "Service id:10000000000619 name:'Deploy Ticket Monster on VMware-20170222-115347' stopping", 4 | "task_id": 10000000194022, 5 | "task_href": "http://localhost:3001/api/tasks/10000000194022", 6 | "href": "http://localhost:3001/api/services/10000000000619" 7 | } 8 | -------------------------------------------------------------------------------- /client/app/states/oidc/oidc-login.state.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function OidcLoginState (routerHelper) { 3 | routerHelper.configureStates(getStates()) 4 | } 5 | 6 | function getStates () { 7 | return { 8 | 'oidc-login': { 9 | parent: 'application', 10 | url: '/oidc_login', 11 | redirectTo: 'login' 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/mock/poweroperations/service_suspend.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "message": "Service id:10000000000619 name:'Deploy Ticket Monster on VMware-20170222-115347' suspended", 4 | "task_id": 10000000194022, 5 | "task_href": "http://localhost:3001/api/tasks/10000000194022", 6 | "href": "http://localhost:3001/api/services/10000000000619" 7 | } 8 | -------------------------------------------------------------------------------- /client/app/states/_states.sass: -------------------------------------------------------------------------------- 1 | // This file should be @imported into client/assets/sass/styles.sass 2 | // 3 | // @import all of your state styles here 4 | 5 | .splash__message 6 | &::before 7 | content: 'ManageIQ Service UI' 8 | 9 | @import '404/404' 10 | @import 'login/login' 11 | @import 'catalogs/details/details' 12 | @import 'orders/details/details' 13 | -------------------------------------------------------------------------------- /tests/mock/poweroperations/service_start_failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": false, 3 | "message": "Service id:10000000000619 name:'Deploy Ticket Monster on VMware-20170222-115347' failed", 4 | "task_id": 10000000194022, 5 | "task_href": "http://localhost:3001/api/tasks/10000000194022", 6 | "href": "http://localhost:3001/api/services/10000000000619" 7 | } 8 | -------------------------------------------------------------------------------- /tests/mock/poweroperations/service_suspend_failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": false, 3 | "message": "Service id:10000000000619 name:'Deploy Ticket Monster on VMware-20170222-115347' failed", 4 | "task_id": 10000000194022, 5 | "task_href": "http://localhost:3001/api/tasks/10000000194022", 6 | "href": "http://localhost:3001/api/services/10000000000619" 7 | } 8 | -------------------------------------------------------------------------------- /client/app/core/config.js: -------------------------------------------------------------------------------- 1 | const DEVEL_DOMAINS = [ 2 | 'localhost', 3 | '127.0.0.1', 4 | '[::1]' 5 | ]; 6 | const isDevel = DEVEL_DOMAINS.includes(window.location.hostname); 7 | 8 | /** @ngInject */ 9 | export function configure($logProvider, $compileProvider) { 10 | $logProvider.debugEnabled(isDevel); 11 | $compileProvider.debugInfoEnabled(isDevel); 12 | } 13 | -------------------------------------------------------------------------------- /client/assets/sass/_patternfly.sass: -------------------------------------------------------------------------------- 1 | // Bootstrap path variables 2 | $icon-font-path: '~bootstrap-sass/assets/fonts/bootstrap/' 3 | // Font Awesome path variables 4 | $fa-font-path: '~font-awesome/fonts' 5 | // Patternfly path variables 6 | $img-path: '~patternfly/dist/img/' 7 | $font-path: '~patternfly/dist/fonts/' 8 | 9 | @import '~patternfly/dist/sass/patternfly' 10 | -------------------------------------------------------------------------------- /tests/utils/utils.spec.js: -------------------------------------------------------------------------------- 1 | function eventFire (el, etype) { 2 | if (angular.isFunction(el.trigger)) { 3 | el.trigger(etype) 4 | } else if (angular.isFunction(el.fireEvent)) { 5 | el.fireEvent('on' + etype) 6 | } else { 7 | const evObj = document.createEvent('Events') 8 | evObj.initEvent(etype, true, false) 9 | el.dispatchEvent(evObj) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/app/states/vms/vms.state.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function VmsState (routerHelper) { 3 | routerHelper.configureStates(getStates()) 4 | } 5 | 6 | function getStates () { 7 | return { 8 | 'vms': { 9 | parent: 'application', 10 | url: '/vms', 11 | redirectTo: 'services.explorer', 12 | template: '' 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/app/states/orders/orders.state.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function OrdersState (routerHelper) { 3 | routerHelper.configureStates(getStates()) 4 | } 5 | 6 | function getStates () { 7 | return { 8 | 'orders': { 9 | parent: 'application', 10 | url: '/orders', 11 | redirectTo: 'orders.explorer', 12 | template: '' 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/app/core/exception/exception.module.js: -------------------------------------------------------------------------------- 1 | import { config, exceptionHandlerProvider } from './exception-handler.provider.js' 2 | import { exception } from './exception.service.js' 3 | 4 | export const ExceptionModule = angular 5 | .module('app.core.exception', []) 6 | .factory('exception', exception) 7 | .provider('exceptionHandler', exceptionHandlerProvider) 8 | .config(config) 9 | .name 10 | -------------------------------------------------------------------------------- /client/app/states/catalogs/catalogs.state.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function CatalogsState (routerHelper) { 3 | routerHelper.configureStates(getStates()) 4 | } 5 | 6 | function getStates () { 7 | return { 8 | 'catalogs': { 9 | parent: 'application', 10 | url: '/catalogs', 11 | redirectTo: 'catalogs.explorer', 12 | template: '' 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # yarn/npm 2 | npm-debug.log 3 | package-lock.json 4 | yarn-error.log 5 | 6 | /node_modules/ 7 | /.pnp.* 8 | /.yarn/* 9 | !/.yarn/patches 10 | !/.yarn/plugins 11 | !/.yarn/releases 12 | !/.yarn/sdks 13 | !/.yarn/versions 14 | 15 | # coverage 16 | reports/ 17 | 18 | ### Project Files ### 19 | .idea* 20 | 21 | # test build 22 | dist/ 23 | 24 | ### Git Version file ### 25 | client/version/version.json 26 | -------------------------------------------------------------------------------- /client/app/shared/loading.component.js: -------------------------------------------------------------------------------- 1 | export const LoadingComponent = { 2 | bindings: { 3 | status: '<' 4 | }, 5 | controllerAs: 'vm', 6 | controller: function () { 7 | const vm = this 8 | vm.config = {icon: 'spinner spinner-lg spinner-inline', title: __('Loading')} 9 | }, 10 | template: 11 | ` 12 | 13 | ` 14 | } 15 | -------------------------------------------------------------------------------- /client/app/states/404/404.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Logo 4 |
5 |

The page you requested cannot be found.

6 |

The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.

7 |

We apologize for the inconvenience.

8 |
9 | -------------------------------------------------------------------------------- /client/app/states/services/services.state.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function ServicesState (routerHelper) { 3 | routerHelper.configureStates(getStates()) 4 | } 5 | 6 | function getStates () { 7 | return { 8 | 'services': { 9 | parent: 'application', 10 | url: '/services', 11 | redirectTo: 'services.explorer', 12 | template: '', 13 | params: { filter: null } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /skin-sample/unlink.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TARGET="" 4 | 5 | if [ -n "$1" ]; then 6 | TARGET="$1" 7 | if ! [ -d "$1" ]; then 8 | echo "Target ($1) is not a directory" 1>&2 9 | exit 1 10 | fi 11 | else 12 | cd "`dirname "$0"`"/.. 13 | TARGET=`pwd` 14 | cd - 15 | fi 16 | 17 | if ! [ -e "$TARGET"/client/skin ]; then 18 | echo "Target ($TARGET) is not skinned" 1>&2 19 | exit 2 20 | fi 21 | 22 | rm -v "$TARGET"/client/skin 23 | -------------------------------------------------------------------------------- /client/app/core/modal/base-modal.factory.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function BaseModalFactory ($uibModal) { 3 | return { 4 | open: openModal 5 | } 6 | 7 | function openModal (overrideOptions) { 8 | var defaultOptions = { 9 | size: 'md' 10 | } 11 | var modalOptions = angular.merge({}, defaultOptions, overrideOptions) 12 | var modal = $uibModal.open(modalOptions) 13 | 14 | return modal.result 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/app/services/usage-graphs/_usage-graphs.sass: -------------------------------------------------------------------------------- 1 | .metric-title 2 | font-size: 24px 3 | text-align: left 4 | 5 | .metric-number 6 | display: block 7 | float: left 8 | font-size: 20px 9 | text-align: left 10 | 11 | .metric-details-container 12 | height: 30px 13 | 14 | .metric-details 15 | display: block 16 | float: left 17 | padding-left: 10px 18 | padding-top: 3px 19 | 20 | p 21 | margin: 0 22 | text-align: left 23 | -------------------------------------------------------------------------------- /client/app/states/catalogs/catalogs.state.spec.js: -------------------------------------------------------------------------------- 1 | /* global $state */ 2 | describe('State: catalogs', () => { 3 | beforeEach(() => { 4 | module('app.states') 5 | bard.inject('$state') 6 | }) 7 | 8 | describe('route', () => { 9 | beforeEach(() => { 10 | bard.inject('$state') 11 | }) 12 | 13 | it('should work with $state.go', () => { 14 | $state.go('catalogs') 15 | expect($state.is('catalogs')) 16 | }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /client/app/states/dashboard/dashboard.state.spec.js: -------------------------------------------------------------------------------- 1 | /* global $state */ 2 | describe('State: dashboard', () => { 3 | beforeEach(() => { 4 | module('app.states') 5 | bard.inject('$state') 6 | }) 7 | 8 | describe('route', () => { 9 | beforeEach(() => { 10 | bard.inject('$state') 11 | }) 12 | 13 | it('should work with $state.go', () => { 14 | $state.go('dashboard') 15 | expect($state.is('dashboard')) 16 | }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /client/app/globals.js: -------------------------------------------------------------------------------- 1 | // overriden from gettext.config once the initialization is done 2 | if (!window.__) { 3 | window.__ = function (str) { 4 | throw new Error([ 5 | 'Attempting to call gettext before the service was initialized.', 6 | 'Maybe you\'re calling it in the .config phase? ("' + str + '")' 7 | ].join(' ')) 8 | } 9 | } 10 | 11 | // N_ is OK anywhere 12 | if (!window.N_) { 13 | window.N_ = function (str) { 14 | return str 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/app/catalogs/catalogs.module.js: -------------------------------------------------------------------------------- 1 | import { CatalogExplorerComponent } from './catalog-explorer.component.js' 2 | import { CatalogsStateFactory } from './catalogs-state.service.js' 3 | import { SharedModule } from '../shared/shared.module.js' 4 | 5 | export const CatalogsModule = angular 6 | .module('app.catalogs', [ 7 | SharedModule 8 | ]) 9 | .component('catalogExplorer', CatalogExplorerComponent) 10 | .factory('CatalogsState', CatalogsStateFactory) 11 | .name 12 | -------------------------------------------------------------------------------- /client/app/components/components.module.js: -------------------------------------------------------------------------------- 1 | import { SharedModule } from '../shared/shared.module.js' 2 | import { DashboardComponent } from './dashboard/dashboard.component.js' 3 | import { DashboardComponentFactory } from './dashboard/dashboard.component.service.js' 4 | 5 | export default angular 6 | .module('app.components', [ 7 | SharedModule 8 | ]) 9 | .component('dashboardComponent', DashboardComponent) 10 | .factory('DashboardService', DashboardComponentFactory) 11 | .name 12 | -------------------------------------------------------------------------------- /language/validate.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const potFile = path.join(__dirname, '../client/gettext/po/manageiq-ui-service.pot') 4 | const contents = fs.readFileSync(potFile, 'utf8') 5 | const re = /<(.|\n)*>/gim // checks for html 6 | const matches = contents.match(re) 7 | 8 | if (matches != null) { 9 | console.log('Errors exist in language file') 10 | console.log(matches) 11 | process.exit(1) 12 | } else { 13 | process.exit(0) 14 | } 15 | -------------------------------------------------------------------------------- /client/app/states/404/404.state.js: -------------------------------------------------------------------------------- 1 | import template from './404.html'; 2 | 3 | /** @ngInject */ 4 | export function NotFoundState (routerHelper) { 5 | var otherwise = '/404' 6 | routerHelper.configureStates(getStates(), otherwise) 7 | } 8 | 9 | function getStates () { 10 | return { 11 | '404': { 12 | parent: 'blank', 13 | url: '/404', 14 | template, 15 | title: '404', 16 | data: { 17 | layout: 'blank' 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /language/validate-language-codes.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const parsed = JSON.parse(fs.readFileSync(path.join(__dirname, '../client/gettext/json/manageiq-ui-service.json'), 'utf8')) 5 | let errors = 0 6 | 7 | Object.keys(parsed).forEach((lang) => { 8 | if (lang.includes('_')) { // language code should not contain underscore 9 | console.log('Invalid language code: ', lang) 10 | errors++ 11 | } 12 | }) 13 | 14 | process.exit(errors) 15 | -------------------------------------------------------------------------------- /config/webpack.testing.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const config = require('./webpack.dev.js'); 4 | 5 | config.module.rules.push({ 6 | test: /\.js$/, 7 | enforce: 'post', 8 | include: `${config.context}/app`, 9 | loader: '@jsdevtools/coverage-istanbul-loader', 10 | exclude: [ 11 | /\.spec\.js$/, 12 | /node_modules/, 13 | ], 14 | }); 15 | 16 | config.output.filename = '[name].js'; 17 | config.output.path = path.resolve(__dirname, '../dist'); 18 | 19 | module.exports = config; 20 | -------------------------------------------------------------------------------- /client/app/states/dashboard/dashboard.state.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function DashboardState (routerHelper, RBAC) { 3 | routerHelper.configureStates(getStates(RBAC)) 4 | } 5 | 6 | function getStates (RBAC) { 7 | return { 8 | 'dashboard': { 9 | parent: 'application', 10 | url: '/', 11 | template: ``, 12 | title: __('Dashboard'), 13 | data: { 14 | authorization: RBAC.has(RBAC.FEATURES.DASHBOARD.VIEW) 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/assets/sass/_functions.sass: -------------------------------------------------------------------------------- 1 | @function active($map) 2 | 3 | @if map-has-key($map, active) 4 | $active-return-val: map-get($map, active) 5 | 6 | @else 7 | @warn 'No `active`' 8 | $active-return-val: null 9 | 10 | @return $active-return-val 11 | 12 | 13 | @function normal($map) 14 | 15 | @if map-has-key($map, normal) 16 | $normal-return-val: map-get($map, normal) 17 | 18 | @else 19 | @warn 'No `normal`' 20 | $normal-return-val: null 21 | 22 | @return $normal-return-val 23 | -------------------------------------------------------------------------------- /.github/workflows/update_yarn_lock.yaml: -------------------------------------------------------------------------------- 1 | name: Update yarn.lock 2 | on: 3 | schedule: 4 | - cron: 0 0 * * 0 5 | workflow_dispatch: 6 | concurrency: 7 | group: "${{ github.workflow }}-${{ github.ref }}" 8 | cancel-in-progress: true 9 | permissions: 10 | contents: read 11 | jobs: 12 | update-yarn-lock: 13 | uses: manageiq/.github/.github/workflows/update_yarn_lock.yaml@master 14 | with: 15 | pr_repository: miq-bot/manageiq-ui-service 16 | secrets: 17 | pr_token: "${{ secrets.PR_TOKEN }}" 18 | -------------------------------------------------------------------------------- /client/app/states/orders/explorer/explorer.state.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function OrdersExplorerState (routerHelper, RBAC) { 3 | routerHelper.configureStates(getStates(RBAC)) 4 | } 5 | 6 | function getStates (RBAC) { 7 | return { 8 | 'orders.explorer': { 9 | url: '', 10 | template: '', 11 | title: __('My Orders'), 12 | data: { 13 | authorization: RBAC.hasAny(['miq_request_show', 'miq_request_show_list']) 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/mock/services/service1_tags.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 10000000000023, 4 | "name": "/managed/environment/dev", 5 | "category": { 6 | "id": 10000000000020 7 | }, 8 | "categorization": { 9 | "displayName": "Environment: Development" 10 | } 11 | }, 12 | { 13 | "id": 10000000000185, 14 | "name": "/managed/function/app", 15 | "category": { 16 | "id": 10000000000006 17 | }, 18 | "categorization": { 19 | "displayName": "Workload: app" 20 | } 21 | } 22 | ] -------------------------------------------------------------------------------- /client/app/states/help/help.state.js: -------------------------------------------------------------------------------- 1 | import template from './help.html'; 2 | 3 | /** @ngInject */ 4 | export function HelpState (routerHelper) { 5 | routerHelper.configureStates(getStates()) 6 | } 7 | 8 | function getStates () { 9 | return { 10 | 'help': { 11 | parent: 'application', 12 | url: '/', 13 | template, 14 | controller: StateController, 15 | controllerAs: 'vm', 16 | title: N_('Help') 17 | } 18 | } 19 | } 20 | 21 | /** @ngInject */ 22 | function StateController () { 23 | } 24 | -------------------------------------------------------------------------------- /client/app/states/services/resource-details/details.state.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function VmsDetailsState (routerHelper, RBAC) { 3 | routerHelper.configureStates(getStates(RBAC)) 4 | } 5 | 6 | function getStates (RBAC) { 7 | return { 8 | 'services.resource-details': { 9 | url: '/:serviceId/resource=:vmId', 10 | template: '', 11 | title: __('Resource Details'), 12 | data: { 13 | authorization: RBAC.has(RBAC.FEATURES.VMS.VIEW) 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/app/shared/ss-card/ss-card.html: -------------------------------------------------------------------------------- 1 |

{{ ::vm.header }}

2 | {{ ::vm.subHeader }} 3 |
4 | 5 | 6 |
7 | 8 | Description 9 | 10 | 11 | -------------------------------------------------------------------------------- /client/app/services/resource-details/_resource-details.sass: -------------------------------------------------------------------------------- 1 | @import 'app_colors' 2 | 3 | .card-pf 4 | overflow-x: auto 5 | 6 | .card-pf-row 7 | margin-bottom: 10px 8 | 9 | .card-pf-regular 10 | max-height: 250px 11 | min-height: 250px 12 | 13 | .card-pf-large 14 | max-height: 380px 15 | min-height: 380px 16 | 17 | .card-pf-info-image 18 | .info-img 19 | height: 50px 20 | 21 | .card-pf-top 22 | .card-pf 23 | max-height: 110px 24 | min-height: 110px 25 | 26 | .pficon-info 27 | color: $app-color-medium-blue-2 28 | -------------------------------------------------------------------------------- /client/app/states/services/details/details.state.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function ServicesDetailsState (routerHelper, RBAC) { 3 | routerHelper.configureStates(getStates(RBAC)) 4 | } 5 | 6 | function getStates (RBAC) { 7 | return { 8 | 'services.details': { 9 | url: '/:serviceId', 10 | template: '', 11 | controllerAs: 'vm', 12 | title: __('Service Details'), 13 | data: { 14 | authorization: RBAC.has(RBAC.FEATURES.SERVICES.VIEW) 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/app/skin/skin.module.js: -------------------------------------------------------------------------------- 1 | const text = { 2 | app: { 3 | name: 'ManageIQ Service UI' 4 | }, 5 | login: { 6 | brand: 'ManageIQ Service UI' 7 | } 8 | } 9 | 10 | export const SkinModule = angular 11 | .module('app.skin', []) 12 | .constant('Text', text) 13 | .config(configure) 14 | .name 15 | 16 | /** @ngInject */ 17 | function configure (routerHelperProvider, exceptionHandlerProvider) { 18 | exceptionHandlerProvider.configure('[ManageIQ] ') 19 | routerHelperProvider.configure({docTitle: 'ManageIQ: '}) 20 | } 21 | -------------------------------------------------------------------------------- /client/app/shared/autofocus.directive.js: -------------------------------------------------------------------------------- 1 | /* 2 | A few browsers still in use today do not fully support HTML5s 'autofocus'. 3 | 4 | This directive is redundant for browsers that do but has no negative effects. 5 | */ 6 | 7 | /** @ngInject */ 8 | export function AutofocusDirective ($timeout) { 9 | var directive = { 10 | restrict: 'A', 11 | link: link 12 | } 13 | 14 | return directive 15 | 16 | function link (_scope, element) { 17 | $timeout(setFocus, 1) 18 | 19 | function setFocus () { 20 | element[0].focus() 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/app/states/about-me/about-me.state.js: -------------------------------------------------------------------------------- 1 | import template from './about-me.html'; 2 | 3 | /** @ngInject */ 4 | export function AboutMeState (routerHelper) { 5 | routerHelper.configureStates(getStates()) 6 | } 7 | 8 | function getStates () { 9 | return { 10 | 'about-me': { 11 | parent: 'application', 12 | url: '/about-me', 13 | template, 14 | controller: StateController, 15 | controllerAs: 'vm', 16 | title: N_('About Me') 17 | } 18 | } 19 | } 20 | 21 | /** @ngInject */ 22 | function StateController () { 23 | } 24 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Import the rake tasks from manageiq core. 2 | # 3 | # HACK: Since we don't have a proper symlink relationship to core like we do 4 | # with other plugins, we have to resort to assuming a sibling directory 5 | # similar to what we do in config/webpack.dev.js. 6 | namespace :app do 7 | load File.join(__dir__, "../manageiq/lib/tasks/test_security.rake") 8 | end 9 | 10 | desc "Rebuild yarn audit pending list" 11 | task :rebuild_yarn_audit_pending do 12 | ENV["ENGINE_ROOT"] = __dir__ 13 | Rake::Task["app:test:security:rebuild_yarn_audit_pending"].invoke 14 | end 15 | -------------------------------------------------------------------------------- /client/app/core/modal/base-modal.factory.spec.js: -------------------------------------------------------------------------------- 1 | /* global ModalService */ 2 | describe('BaseModalFactory', () => { 3 | beforeEach(function () { 4 | module('app.core') 5 | bard.inject('ModalService') 6 | }) 7 | 8 | it('shoud allow a modal to be opened', () => { 9 | const modalOptions = { 10 | component: 'editServiceModal', 11 | resolve: { 12 | service: function () { 13 | return true 14 | } 15 | } 16 | } 17 | const modal = ModalService.open(modalOptions) 18 | expect(modal).to.be.a('object') 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /client/app/shared/language-switcher/_language-switcher.sass: -------------------------------------------------------------------------------- 1 | @import 'colors' 2 | 3 | .login-dropdown 4 | .dropdown-menu 5 | height: 180px 6 | overflow-y: scroll 7 | 8 | .language-switcher 9 | a 10 | clear: both 11 | color: $color-gray-4 12 | display: block 13 | line-height: 1.6667 14 | padding: 1px 10px 15 | 16 | &:hover 17 | text-decoration: none 18 | 19 | ul 20 | &.scrollable-menu 21 | top: -22px 22 | 23 | .switch-language-link 24 | &::after 25 | display: inline 26 | float: right 27 | position: inherit 28 | -------------------------------------------------------------------------------- /client/app/states/services/explorer/explorer.state.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function ServicesExplorerState (routerHelper, RBAC) { 3 | routerHelper.configureStates(getStates(RBAC)) 4 | } 5 | 6 | function getStates (RBAC) { 7 | return { 8 | 'services.explorer': { 9 | url: '', 10 | template: '', 11 | controllerAs: 'vm', 12 | title: __('Services Explorer'), 13 | params: { filter: null }, 14 | data: { 15 | authorization: RBAC.has(RBAC.FEATURES.SERVICES.VIEW) 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/app/states/404/404.state.spec.js: -------------------------------------------------------------------------------- 1 | /* global $state, $location */ 2 | describe('State: 404', () => { 3 | beforeEach(module('app.states')) 4 | 5 | describe('route', () => { 6 | beforeEach(() => { 7 | bard.inject('$location', '$state') 8 | }) 9 | 10 | it('should map /404 route to 404 View template', () => { 11 | expect($state.get('404').template).to.match(/blank-slate-pf/) // 404.html topmost classname 12 | }) 13 | 14 | it('should work with $state.go', () => { 15 | $state.go('404') 16 | expect($state.is('404')) 17 | }) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /tests/mock/services/serviceDetailsAnsibleComponent.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 12345, 3 | "options": { 4 | "config_info": { 5 | "test":{ 6 | "credential_id": "testing", 7 | "repository_id": "testrepo" 8 | } 9 | } 10 | }, 11 | "service_resources":[ 12 | { 13 | "name":"test", 14 | "resource_id": 1 15 | } 16 | ], 17 | "orchestration_stacks":[ 18 | { 19 | "id":1, 20 | "stack":{ 21 | "id":12345 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /client/app/states/error/error.state.spec.js: -------------------------------------------------------------------------------- 1 | /* global $state */ 2 | describe('State: error', () => { 3 | beforeEach(() => { 4 | module('app.states') 5 | }) 6 | 7 | describe('route', () => { 8 | beforeEach(() => { 9 | bard.inject('$state') 10 | }) 11 | 12 | it('should map /error route to http-error View template', () => { 13 | expect($state.get('error').template).to.match(/four0four/) // error.html topmost classname 14 | }) 15 | 16 | it('should work with $state.go', () => { 17 | $state.go('error') 18 | expect($state.is('error')) 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /config/githash.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('shelljs'); 2 | const { existsSync, readFileSync, writeFileSync } = require('fs'); 3 | const { join } = require('path'); 4 | 5 | const input = join(__dirname, '../BUILD'); 6 | const output = join(__dirname, '../client/version/version.json'); 7 | 8 | let gitCommit = existsSync(input) 9 | ? readFileSync(input, { encoding: 'utf-8' }) 10 | : exec('git rev-parse HEAD', { silent: true }).stdout; 11 | gitCommit = gitCommit.replace("\n", ''); 12 | 13 | writeFileSync(output, JSON.stringify({ gitCommit })); 14 | 15 | console.log(`Successfully wrote Git hash - ${gitCommit}`); 16 | -------------------------------------------------------------------------------- /client/app/services/service-details/service-details-ansible-modal.component.js: -------------------------------------------------------------------------------- 1 | import template from './service-details-ansible-modal.html'; 2 | 3 | export const ServiceDetailsAnsibleModalComponent = { 4 | controller: ComponentController, 5 | controllerAs: 'vm', 6 | bindings: { 7 | resolve: '<', 8 | close: '&', 9 | dismiss: '&' 10 | }, 11 | template, 12 | } 13 | 14 | /** @ngInject */ 15 | function ComponentController () { 16 | const vm = this 17 | 18 | angular.extend(vm, { 19 | cancel: cancel 20 | }) 21 | 22 | function cancel () { 23 | vm.dismiss({$value: 'cancel'}) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | - When running ManageIQ with `bin/rake evm:start`, it may be necessary to override the REST API host via a 3 | PROXY\_HOST environment variable. 4 | - `PROXY_HOST=127.0.0.1:3000 yarn start` 5 | 6 | - `ActiveRecord::ConnectionTimeoutError: could not obtain a connection from the pool within 5.000 seconds; all pooled 7 | connections were in use` or `Error: socket hang up` or ` Error: connect ECONNREFUSED` 8 | might be caused to by lower than expected connection pool size this is remedied by navigating to 9 | `manageiq/config/database.yml` and increasing the `pool: xx` value. 10 | -------------------------------------------------------------------------------- /bin/before_install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -n "$ACT" ]; then 4 | # Install yarn 5 | curl -o- -L https://yarnpkg.com/install.sh | bash 6 | echo "$HOME/.yarn/bin" >> $GITHUB_PATH 7 | echo "$HOME/.config/yarn/global/node_modules/.bin" >> $GITHUB_PATH 8 | 9 | # Install google-chrome-stable 10 | wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - 11 | sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' 12 | sudo apt-get update -y 13 | sudo apt-get install -y google-chrome-stable 14 | echo 15 | fi 16 | -------------------------------------------------------------------------------- /skin-sample/skin.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict' 3 | 4 | var text = { 5 | app: { 6 | name: 'Magic UI' // app name 7 | }, 8 | login: { 9 | brand: 'The Amazing Magic UI' // login screen 10 | } 11 | } 12 | 13 | angular.module('app.skin', []) 14 | .constant('Text', text) 15 | .config(configure) 16 | 17 | /** @ngInject */ 18 | function configure (routerHelperProvider, exceptionHandlerProvider) { 19 | exceptionHandlerProvider.configure('[MAGIC] ') // error prefix 20 | routerHelperProvider.configure({docTitle: 'Magic UI: '}) // page title 21 | } 22 | })() 23 | -------------------------------------------------------------------------------- /client/console/common.js: -------------------------------------------------------------------------------- 1 | import languageFile from '../gettext/json/manageiq-ui-service.json' 2 | 3 | export default function() { 4 | const urlParams = new URLSearchParams(window.location.search) 5 | 6 | const params = ['path', 'is_vcloud', 'vmx', 'lang'].reduce((map, obj) => { 7 | map[obj] = urlParams.get(obj) 8 | return map 9 | }, {}) 10 | 11 | window.__ = (string) => (languageFile[params.lang] || {})[string] || string 12 | 13 | document.querySelector('#connection-status').innerHTML = __('Connecting') 14 | document.querySelector('#ctrlaltdel').title = __('Send CTRL+ALT+DEL') 15 | 16 | return params 17 | } 18 | -------------------------------------------------------------------------------- /client/assets/sass/_bem-support/_functions.scss: -------------------------------------------------------------------------------- 1 | @function containsModifier($selector) { 2 | $selector: selectorToString($selector); 3 | @if str-index($selector, $modifier-separator) { 4 | @return true; 5 | } @else { 6 | @return false; 7 | } 8 | } 9 | 10 | @function selectorToString($selector) { 11 | $selector: inspect($selector); 12 | $selector: str-slice($selector, 2, -2); 13 | @return $selector; 14 | } 15 | 16 | @function getBlock($selector) { 17 | $selector: selectorToString($selector); 18 | $modifier-start: str-index($selector, $modifier-separator) - 1; 19 | @return str-slice($selector, 0, $modifier-start); 20 | } 21 | -------------------------------------------------------------------------------- /client/app/core/polling.service.spec.js: -------------------------------------------------------------------------------- 1 | /* global Polling */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('Polling Service', () => { 4 | beforeEach(function () { 5 | module('app.shared') 6 | bard.inject('Polling') 7 | Polling.start('test', function () { console.log('hello world') }, 1500) 8 | }) 9 | it('should allow for polling to be created', () => { 10 | const polls = Polling.getPolls() 11 | expect(polls).to.have.property('test') 12 | }) 13 | it('should allow you to stop all polls', () => { 14 | Polling.stopAll() 15 | const polls = Polling.getPolls() 16 | expect(polls).to.be.empty 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /client/app/shared/pagination/_pagination.sass: -------------------------------------------------------------------------------- 1 | @import 'app_colors' 2 | 3 | .pagination-footer 4 | background-color: $app-color-white 5 | border-top: 1px solid $app-color-gray 6 | bottom: 0 7 | height: 50px 8 | padding: 12px 5px 15px 15px 9 | position: fixed 10 | width: 100% 11 | 12 | .pagination-controls 13 | margin-bottom: 0 14 | position: fixed 15 | right: 15px 16 | 17 | .pagination 18 | display: block 19 | margin-top: 0 20 | 21 | > .disabled > span 22 | &:hover 23 | cursor: not-allowed 24 | 25 | .checkall-margin-override 26 | margin-left: 18px 27 | 28 | &.checkbox 29 | margin-top: 2px 30 | -------------------------------------------------------------------------------- /client/app/states/vms/snapshots/snapshots.state.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function VmsSnapshotsState (routerHelper) { 3 | routerHelper.configureStates(getStates()) 4 | } 5 | 6 | function getStates () { 7 | return { 8 | 'vms.snapshots': { 9 | url: '/:vmId/snapshots', 10 | template: '', 11 | controller: StateController, 12 | controllerAs: 'vm', 13 | title: N_('VM Snapshots') 14 | 15 | } 16 | } 17 | } 18 | 19 | /** @ngInject */ 20 | function StateController ($stateParams) { 21 | const vm = this 22 | angular.extend(vm, { 23 | vmId: $stateParams.vmId 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /client/app/states/error/error.html: -------------------------------------------------------------------------------- 1 |
2 | 7 | 8 |
9 | 10 |
11 | There was a problem loading the page. 12 |
13 |

We apologize for the inconvenience.

14 |
15 |
{{vm.error.status}}
16 |
{{vm.error.statusText}}
17 |
18 |
19 |
20 | 21 | -------------------------------------------------------------------------------- /tests/mock/services/servicePermissions.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "edit": true, 4 | "delete": true, 5 | "reconfigure": true, 6 | "setOwnership": true, 7 | "retire": true, 8 | "setRetireDate": true, 9 | "editTags": true, 10 | "viewAnsible": true, 11 | "instanceStart": true, 12 | "instanceStop": true, 13 | "instanceSuspend": true, 14 | "instanceRetire": true, 15 | "console": true, 16 | "viewSnapshots": true, 17 | "vm_snapshot_show_list": true, 18 | "ems_infra_show": true, 19 | "ems_cluster_show": true, 20 | "host_show": true, 21 | "resource_pool_show": true, 22 | "storage_show_list": true, 23 | "instance_show": true, 24 | "vm_drift": true, 25 | "vm_check_compliance": true 26 | } 27 | -------------------------------------------------------------------------------- /client/assets/images/os/os-windows_generic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-scvmm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-hyper-v.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-microsoft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/app/shared/language-switcher/language-switcher.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Switch Language: 5 | 6 | 7 | 14 | 15 | 17 | -------------------------------------------------------------------------------- /client/app/orders/process-order-modal/process-order-modal.component.spec.js: -------------------------------------------------------------------------------- 1 | /* global inject */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('Component: processOrderModal', () => { 4 | beforeEach(() => { 5 | module('app.core', 'app.orders') 6 | }) 7 | 8 | describe('controller', () => { 9 | let ctrl, $componentController 10 | 11 | beforeEach(inject((_$componentController_) => { 12 | var bindings = {resolve: {order: []}} 13 | $componentController = _$componentController_ 14 | ctrl = $componentController('processOrderModal', null, bindings) 15 | })) 16 | 17 | it('should be defined', () => { 18 | expect(ctrl).to.exist 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /client/app/shared/substitute.filter.spec.js: -------------------------------------------------------------------------------- 1 | /* global inject */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('Filter: substitute', () => { 4 | let substituteFilter 5 | 6 | // load the module 7 | beforeEach(module('app.shared')) 8 | 9 | // load filter function into variable 10 | beforeEach(inject(function ($filter) { 11 | substituteFilter = $filter('substitute') 12 | })) 13 | 14 | it('should exist when invoked', () => { 15 | expect(substituteFilter).to.exist 16 | }) 17 | 18 | it('should correctly display valid format', () => { 19 | expect(substituteFilter('Clear All [[heading]]', {heading: 'Notifications'})).to.be.eq('Clear All Notifications') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /client/app/shared/icon-list/_icon-list.sass: -------------------------------------------------------------------------------- 1 | @import '_bem-support/index' 2 | 3 | +block(icon-list) 4 | +element(container) 5 | +element(background) 6 | background: url('../../../assets/images/quadicon-single.png') 7 | height: 72px 8 | width: 72px 9 | 10 | +element(icon) 11 | border: 0 12 | border-radius: 10px 13 | height: 60px 14 | margin: 6px 0 0 6px 15 | position: absolute 16 | width: 60px 17 | 18 | +element(name) 19 | overflow: hidden 20 | text-align: center 21 | text-overflow: ellipsis 22 | white-space: nowrap 23 | width: 72px 24 | 25 | height: 100px 26 | width: 100px 27 | 28 | display: inline-flex 29 | -------------------------------------------------------------------------------- /skin-sample/link.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SOURCE="" 4 | TARGET="" 5 | 6 | cd "`dirname "$0"`" 7 | SOURCE=`pwd` 8 | cd - 9 | 10 | if [ -n "$1" ]; then 11 | TARGET="$1" 12 | if ! [ -d "$1" ]; then 13 | echo "Target ($1) is not a directory" 1>&2 14 | exit 1 15 | fi 16 | else 17 | cd "`dirname "$0"`"/.. 18 | TARGET=`pwd` 19 | cd - 20 | fi 21 | 22 | if ! [ -d "$TARGET"/client ]; then 23 | echo "Target ($TARGET) is not the Service UI directory (missing client/)" 1>&2 24 | exit 1 25 | fi 26 | 27 | if [ -e "$TARGET"/client/skin ]; then 28 | echo "Target ($TARGET) is already skinned" 1>&2 29 | ls -ld "$TARGET"/client/skin 30 | exit 2 31 | fi 32 | 33 | ln -vsf "$SOURCE" "$TARGET"/client/skin 34 | -------------------------------------------------------------------------------- /client/app/shared/format-bytes.filter.js: -------------------------------------------------------------------------------- 1 | export function formatBytes () { 2 | return function (bytes) { 3 | if (bytes === 0) { 4 | return '0 Bytes' 5 | } 6 | if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) { 7 | return '-' 8 | } 9 | const availableUnits = ['Bytes', 'kB', 'MB', 'GB', 'TB', 'PB'] 10 | const unit = Math.floor(Math.log(bytes) / Math.log(1024)) 11 | const val = (bytes / Math.pow(1024, Math.floor(unit))).toFixed(2) 12 | 13 | return `${val.match(/\.0*$/) ? val.substr(0, val.indexOf('.')) : val} ${availableUnits[unit]}` 14 | } 15 | } 16 | 17 | export function megaBytes () { 18 | return function (bytes) { 19 | return bytes * 1024 * 1024 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/app/core/layouts.config.js: -------------------------------------------------------------------------------- 1 | import applicationTemplate from '../layouts/application.html'; 2 | import blankTemplate from '../layouts/blank.html'; 3 | 4 | /** @ngInject */ 5 | export function layoutInit (routerHelper) { 6 | routerHelper.configureStates(getLayouts()) 7 | } 8 | 9 | function getLayouts () { 10 | return { 11 | 'blank': { 12 | abstract: true, 13 | template: blankTemplate, 14 | }, 15 | 'application': { 16 | abstract: true, 17 | template: applicationTemplate, 18 | onExit: exitApplication, 19 | }, 20 | } 21 | } 22 | 23 | /** @ngInject */ 24 | function exitApplication (Polling) { 25 | // Remove all of the navigation polls 26 | Polling.stopAll() 27 | } 28 | -------------------------------------------------------------------------------- /client/app/orders/request-list/requests-list.component.spec.js: -------------------------------------------------------------------------------- 1 | /* global inject */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('Component: requests-list', () => { 4 | beforeEach(function () { 5 | module('app.core', 'app.orders') 6 | }) 7 | 8 | let $compile, scope 9 | 10 | beforeEach(inject(($rootScope, _$compile_) => { 11 | scope = $rootScope.$new() 12 | $compile = _$compile_ 13 | })) 14 | 15 | it('should be defined', () => { 16 | scope['items'] = [] 17 | scope['config'] = [] 18 | 19 | let element = angular.element(``) 20 | element = $compile(element)(scope) 21 | scope.$digest() 22 | expect(element).to.exist 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Any contribution should observe the convention set fourth in the [Code Tour](code_tour.md). 3 | The following must complete without error prior to making a pull request: 4 | 5 | 1. `yarn vet` 6 | 2. `yarn test` 7 | 8 | ## Testing 9 | When adding or modifying functionality, unit and integration tests must accompany any changes. 10 | Examples of both state and component tests can be found in `tests/`. 11 | If any notional data is required for the test ensure an appropriately titled folder is created in `tests/mock/`. 12 | 13 | If you would like to run unit tests for the project, you can run 14 | ```yarn test ``` 15 | 16 | ## How To... 17 | * [Add a Functional Zone](./howto.md#Addafunctionalzone) 18 | -------------------------------------------------------------------------------- /client/app/states/catalogs/explorer/explorer.state.js: -------------------------------------------------------------------------------- 1 | import template from './catalogs.html'; 2 | 3 | /** @ngInject */ 4 | export function CatalogsExplorerState (routerHelper, RBAC) { 5 | routerHelper.configureStates(getStates(RBAC)) 6 | } 7 | 8 | function getStates (RBAC) { 9 | return { 10 | 'catalogs.explorer': { 11 | url: '', 12 | template, 13 | controller: CatalogsController, 14 | controllerAs: 'vm', 15 | title: __('Catalogs'), 16 | data: { 17 | authorization: RBAC.hasAny(['svc_catalog_provision', 'sui_svc_catalog_view']) 18 | } 19 | } 20 | } 21 | } 22 | 23 | /** @ngInject */ 24 | function CatalogsController () { 25 | const vm = this 26 | vm.title = __('Catalogs') 27 | } 28 | -------------------------------------------------------------------------------- /client/app/app.module.js: -------------------------------------------------------------------------------- 1 | import './globals.js' 2 | import './components/components.module.js' 3 | 4 | import { AppController } from './app.controller.js' 5 | import { AppRoutingModule } from './states/states.module.js' 6 | import { CatalogsModule } from './catalogs/catalogs.module.js' 7 | import { CoreModule } from './core/core.module.js' 8 | import { RequestsModule } from './orders/orders.module.js' 9 | import { ServicesModule } from './services/services.module.js' 10 | 11 | export default angular 12 | .module('app', [ 13 | 'ngProgress', 14 | 15 | AppRoutingModule, 16 | CoreModule, 17 | 18 | // Feature Modules 19 | CatalogsModule, 20 | RequestsModule, 21 | ServicesModule 22 | ]) 23 | .controller('AppController', AppController) 24 | .name 25 | -------------------------------------------------------------------------------- /client/app/states/error/error.state.js: -------------------------------------------------------------------------------- 1 | import template from './error.html'; 2 | 3 | /** @ngInject */ 4 | export function ErrorState (routerHelper) { 5 | var otherwise = '/error' 6 | routerHelper.configureStates(getStates(), otherwise) 7 | } 8 | 9 | function getStates () { 10 | return { 11 | 'error': { 12 | parent: 'blank', 13 | url: '/error', 14 | template, 15 | controller: StateController, 16 | controllerAs: 'vm', 17 | title: N_('Error'), 18 | data: { 19 | layout: 'blank' 20 | }, 21 | params: { 22 | error: null 23 | } 24 | } 25 | } 26 | } 27 | 28 | /** @ngInject */ 29 | function StateController ($stateParams) { 30 | const vm = this 31 | 32 | vm.error = $stateParams.error 33 | } 34 | -------------------------------------------------------------------------------- /client/app/orders/orders.module.js: -------------------------------------------------------------------------------- 1 | import { OrderExplorerComponent } from './order-explorer/order-explorer.component.js' 2 | import { OrdersStateFactory } from './orders-state.service.js' 3 | import { ProcessOrderModalComponent } from './process-order-modal/process-order-modal.component.js' 4 | import { RequestsListComponent } from './request-list/requests-list.component.js' 5 | import { SharedModule } from '../shared/shared.module.js' 6 | 7 | export const RequestsModule = angular 8 | .module('app.orders', [ 9 | SharedModule 10 | ]) 11 | .component('processOrderModal', ProcessOrderModalComponent) 12 | .component('orderExplorer', OrderExplorerComponent) 13 | .component('requestsList', RequestsListComponent) 14 | .factory('OrdersState', OrdersStateFactory) 15 | .name 16 | -------------------------------------------------------------------------------- /client/app/core/site-switcher/site-switcher.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 13 |
14 | -------------------------------------------------------------------------------- /client/app/orders/process-order-modal/process-order-modal.html: -------------------------------------------------------------------------------- 1 | 7 | 14 | 21 | 22 | -------------------------------------------------------------------------------- /client/app/shared/elapsedTime.filter.spec.js: -------------------------------------------------------------------------------- 1 | /* global inject */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('Filter: elapsedTimeFilter', () => { 4 | let elapsedTimeFilter 5 | 6 | // load the module 7 | beforeEach(module('app.shared')) 8 | 9 | // load filter function into variable 10 | beforeEach(inject(function ($filter) { 11 | elapsedTimeFilter = $filter('elapsedTime') 12 | })) 13 | 14 | it('should exist when invoked', () => { 15 | expect(elapsedTimeFilter).to.exist 16 | }) 17 | 18 | it('should correctly display valid time format', () => { 19 | expect(elapsedTimeFilter(120000)).to.be.eq('33 hours 20 min 00 sec') 20 | }) 21 | 22 | it('should correctly display invalid time format', () => { 23 | expect(elapsedTimeFilter()).to.be.eq('00:00:00') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /client/app/shared/elapsedTime.filter.js: -------------------------------------------------------------------------------- 1 | // Accepts a value of seconds, returns sting of hours minutes seconds 2 | export function ElapsedTime () { 3 | return function (time) { 4 | if (!angular.isNumber(time) || time < 0) { 5 | return '00:00:00' 6 | } 7 | 8 | const hours = Math.floor(time / 3600) 9 | const minutes = Math.floor((time % 3600) / 60) 10 | const seconds = Math.floor(time % 60) 11 | 12 | if (hours > 0) { 13 | return padding(hours) + ' hours ' + padding(minutes) + ' min ' + padding(seconds) + ' sec' 14 | } else if (minutes > 0) { 15 | return padding(minutes) + ' min ' + padding(seconds) + ' sec' 16 | } else { 17 | return padding(seconds) + ' sec' 18 | } 19 | } 20 | 21 | function padding (t) { 22 | return t < 10 ? '0' + t : t 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/app/shared/icon-list/icon-list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{ item.name }} 6 | {{ item.name }} 8 |
9 |
{{item.name}}
10 |
{{item.description}}
11 |
12 |
13 | -------------------------------------------------------------------------------- /client/assets/sass/_splash.sass: -------------------------------------------------------------------------------- 1 | +block(splash) 2 | 3 | +element(overlay) 4 | backface-visibility: hidden 5 | background-color: $app-color-dark-navy-blue 6 | bottom: 0 7 | left: 0 8 | opacity: .9 9 | pointer-events: none 10 | position: fixed 11 | right: 0 12 | top: 0 13 | transition: opacity .3s linear 14 | z-index: 9999 15 | 16 | +element(message) 17 | color: $app-color-gray 18 | font-family: Lato, Helvetica Neue, Helvetica, Arial, sans-serif 19 | font-size: 400% 20 | font-weight: normal 21 | margin: 20% auto 0 22 | text-align: center 23 | text-decoration: none 24 | text-shadow: 2px 2px $app-color-dark-gray 25 | text-transform: uppercase 26 | 27 | display: none 28 | opacity: 0 29 | 30 | .ng-cloak 31 | &.splash 32 | display: block 33 | opacity: 1 34 | -------------------------------------------------------------------------------- /client/app/states/404/_404.sass: -------------------------------------------------------------------------------- 1 | +block(four0four) 2 | 3 | +element(window) 4 | background-color: $app-color-light-gray 5 | border: 2px solid $app-color-gray 6 | border-radius: 6px 7 | box-shadow: 0 10px 10px $app-color-dark-gray 8 | color: $app-color-dark-gray 9 | left: 50% 10 | margin: 0 auto 11 | max-width: 80% 12 | min-width: 600px 13 | padding: 40px 14 | position: absolute 15 | top: 50% 16 | transform: translate(-50%, -50%) 17 | 18 | +element(title) 19 | font-size: 1.4em 20 | font-weight: bold 21 | padding-bottom: 20px 22 | text-transform: uppercase 23 | 24 | +element(logo) 25 | border: 1px solid $app-color-gray 26 | border-radius: 4px 27 | box-shadow: 0 3px 5px $app-color-dark-gray 28 | display: block 29 | margin: 0 auto 20px 30 | 31 | font-size: 16px 32 | -------------------------------------------------------------------------------- /client/app/states/services/details/details.state.spec.js: -------------------------------------------------------------------------------- 1 | /* global $state, $componentController */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('State: services.details', () => { 4 | beforeEach(() => { 5 | module('app.states', 'app.services') 6 | bard.inject('$state') 7 | }) 8 | 9 | describe('route', () => { 10 | it('should work with $state.go', () => { 11 | $state.go('services.details') 12 | expect($state.is('services.details')) 13 | }) 14 | }) 15 | 16 | describe('controller', () => { 17 | let controller 18 | 19 | beforeEach(() => { 20 | bard.inject('$componentController') 21 | 22 | controller = $componentController('serviceExplorer', {}) 23 | }) 24 | 25 | it('should be created successfully', () => { 26 | expect(controller).to.exist 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /client/app/states/error/_error.sass: -------------------------------------------------------------------------------- 1 | +block(four0four) 2 | 3 | +element(window) 4 | background-color: $app-color-light-gray 5 | border: 2px solid $app-color-gray 6 | border-radius: 6px 7 | box-shadow: 0 10px 10px $app-color-dark-gray 8 | color: $app-color-dark-gray 9 | left: 50% 10 | margin: 0 auto 11 | max-width: 80% 12 | min-width: 600px 13 | padding: 40px 14 | position: absolute 15 | top: 50% 16 | transform: translate(-50%, -50%) 17 | 18 | +element(title) 19 | font-size: 1.4em 20 | font-weight: bold 21 | padding-bottom: 20px 22 | text-transform: uppercase 23 | 24 | +element(logo) 25 | border: 1px solid $app-color-gray 26 | border-radius: 4px 27 | box-shadow: 0 3px 5px $app-color-dark-gray 28 | display: block 29 | margin: 0 auto 20px 30 | 31 | font-size: 16px 32 | -------------------------------------------------------------------------------- /client/app/states/vms/snapshots/snapshots.state.spec.js: -------------------------------------------------------------------------------- 1 | /* global $state, $controller */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('State: vms.snapshots', () => { 4 | beforeEach(() => { 5 | module('app.states') 6 | }) 7 | 8 | describe('controller', () => { 9 | let ctrl 10 | 11 | beforeEach(() => { 12 | bard.inject('$controller', '$state', '$stateParams') 13 | 14 | ctrl = $controller($state.get('vms.snapshots').controller, { 15 | $stateParams: { 16 | vmId: 123 17 | } 18 | }) 19 | }) 20 | 21 | describe('controller initialization', () => { 22 | it('is created successfully', () => { 23 | expect(ctrl).to.exist 24 | }) 25 | 26 | it('sets stateParams vmId', () => { 27 | expect(ctrl.vmId).to.equal(123) 28 | }) 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /client/app/services/usage-graphs/usage-graphs.service.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function UsageGraphsFactory () { 3 | var service = { 4 | convertBytestoGb: convertBytestoGb, 5 | getChartConfig: getChartConfig 6 | } 7 | 8 | function getChartConfig (config, used, total) { 9 | let usedValue = 0 10 | let totalValue = 0 11 | 12 | if (angular.isDefined(used)) { 13 | usedValue = used 14 | totalValue = total 15 | } 16 | 17 | return { 18 | config: { 19 | units: config.units, 20 | chartId: config.chartId 21 | }, 22 | data: { 23 | 'used': usedValue, 24 | 'total': totalValue 25 | }, 26 | label: config.label 27 | } 28 | } 29 | 30 | function convertBytestoGb (bytes) { 31 | return Number((bytes / 1073741824).toFixed(2)) 32 | } 33 | 34 | return service 35 | } 36 | -------------------------------------------------------------------------------- /client/app/shared/tagging/_tagging.sass: -------------------------------------------------------------------------------- 1 | @import 'app_colors' 2 | 3 | .tag-spinner 4 | float: left 5 | margin-left: 136px 6 | margin-top: 6px 7 | 8 | .tag-category-select 9 | float: left 10 | width: 180px 11 | 12 | .no-tag-header 13 | font-style: italic 14 | font-weight: 400 15 | padding-top: 30px 16 | 17 | .tag-value-select 18 | float: left 19 | margin-left: 14px 20 | width: 180px 21 | 22 | .tag-add 23 | color: $color-lochmara-approx 24 | cursor: pointer 25 | float: right 26 | font-size: 18px 27 | 28 | .tag-list 29 | border-top: 0 30 | padding-top: 35px 31 | 32 | &.tag-list-no-top-controls 33 | padding-top: 0 34 | 35 | li 36 | display: inline-block 37 | padding-bottom: 11px 38 | padding-right: 2px 39 | 40 | .label 41 | font-size: 11px 42 | 43 | a 44 | color: $color-white 45 | cursor: pointer 46 | -------------------------------------------------------------------------------- /client/app/core/list-configuration.service.js: -------------------------------------------------------------------------------- 1 | /* eslint camelcase: "off" */ 2 | 3 | /** @ngInject */ 4 | export function ListConfigurationFactory () { 5 | var configuration = {} 6 | 7 | configuration.setupListFunctions = function (list, currentField) { 8 | list.sort = { 9 | isAscending: true, 10 | currentField: currentField 11 | } 12 | 13 | list.filters = [] 14 | 15 | list.setSort = function (currentField, isAscending) { 16 | list.sort.isAscending = isAscending 17 | list.sort.currentField = currentField 18 | } 19 | 20 | list.getSort = function () { 21 | return list.sort 22 | } 23 | 24 | list.setFilters = function (filterArray) { 25 | list.filters = filterArray 26 | } 27 | 28 | list.getFilters = function () { 29 | return list.filters 30 | } 31 | } 32 | 33 | return configuration 34 | } 35 | -------------------------------------------------------------------------------- /client/app/shared/confirmation/confirmation.component.spec.js: -------------------------------------------------------------------------------- 1 | /* global inject */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('Directive: confirmation', () => { 4 | beforeEach(module('app.shared')) 5 | describe('template', () => { 6 | let parentScope, $compile 7 | 8 | beforeEach(inject((_$compile_, _$rootScope_) => { 9 | $compile = _$compile_ 10 | parentScope = _$rootScope_.$new() 11 | })) 12 | 13 | const compileHtml = function (markup, scope) { 14 | let element = angular.element(markup) 15 | $compile(element)(scope) 16 | scope.$digest() 17 | return element 18 | } 19 | 20 | it('should compile confirmation when invoked', () => { 21 | const element = compileHtml(angular.element(``), parentScope) 22 | 23 | expect(element[0]).to.exist 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /client/app/app.controller.js: -------------------------------------------------------------------------------- 1 | export class AppController { 2 | constructor ($scope, ngProgressFactory) { 3 | 'ngInject' 4 | 5 | this.progressbar = ngProgressFactory.createInstance() 6 | this.progressbar.setColor('#0088ce') 7 | this.progressbar.setHeight('3px') 8 | 9 | this.$scope = $scope 10 | } 11 | 12 | $onInit () { 13 | this.$scope.$on('$stateChangeStart', (_event, toState) => { 14 | if (toState.resolve) { 15 | this.progressbar.start() 16 | } 17 | }) 18 | 19 | this.$scope.$on('$stateChangeSuccess', (_event, toState) => { 20 | if (toState.resolve) { 21 | this.progressbar.complete() 22 | } 23 | }) 24 | } 25 | 26 | keyDown (evt) { 27 | this.$scope.$broadcast('bodyKeyDown', {origEvent: evt}) 28 | } 29 | 30 | keyUp (evt) { 31 | this.$scope.$broadcast('bodyKeyUp', {origEvent: evt}) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-parallels.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-ansible_tower_configuration.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /client/app/layouts/_navigation.sass: -------------------------------------------------------------------------------- 1 | .navbar-pf-vertical 2 | background-color: $app-color-dark-blue-gray 3 | background-image: url('#{$img-base-path}images/bg-navbar.png') 4 | background-repeat: no-repeat 5 | background-size: auto 100% 6 | 7 | .navbar-brand 8 | .navbar-brand-icon 9 | height: 20px 10 | margin-top: 10px 11 | 12 | .navbar-user-name 13 | color: $color-white 14 | display: inline-block 15 | margin-left: 1ex 16 | 17 | .secondary-collapse-toggle-pf 18 | &::before 19 | content: '\f08d' 20 | 21 | &.collapsed 22 | &::before 23 | content: '\f08d' 24 | display: inline-block 25 | transform: rotate(45deg) 26 | 27 | .toast-notifications-list-pf 28 | top: 65px 29 | 30 | .miq-siteswitcher 31 | button 32 | padding-top: 19px 33 | 34 | .dropdown-menu 35 | margin-top: 7px 36 | min-width: 150px 37 | text-align: center 38 | 39 | -------------------------------------------------------------------------------- /client/app/core/navigation.service.spec.js: -------------------------------------------------------------------------------- 1 | /* global readJSON, RBAC, Navigation, CollectionsApi */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('Navigation Service', () => { 4 | const permissions = readJSON('tests/mock/rbac/allPermissions.json') 5 | beforeEach(function () { 6 | module('app.core') 7 | bard.inject('Navigation', 'RBAC', 'CollectionsApi') 8 | RBAC.set(permissions) 9 | }) 10 | 11 | it('should allow navigation to be setup', () => { 12 | const navigationItems = Navigation.init() 13 | expect(navigationItems.length).to.eq(4) 14 | }) 15 | it('should refresh badge counts', (done) => { 16 | Navigation.init() 17 | const collectionsApiSpy = sinon.stub(CollectionsApi, 'query').returns(Promise.resolve({subcounts: 2})) 18 | Navigation.updateBadgeCounts() 19 | done() 20 | 21 | expect(collectionsApiSpy).to.have.been.calledThrice 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /client/app/services/process-snapshots-modal/process-snapshots-modal.component.spec.js: -------------------------------------------------------------------------------- 1 | /* global inject */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('Component: processSnapshotsModal', () => { 4 | beforeEach(module('app')) 5 | 6 | describe('with $componentController', () => { 7 | const bindings = { 8 | resolve: { 9 | vm: {id: 10000000001457}, 10 | modalData: {name: 'Snapshot', description: 'A test snapshot', memory: 128} 11 | } 12 | } 13 | let scope, ctrl 14 | 15 | beforeEach(inject(function ($componentController) { 16 | ctrl = $componentController('processSnapshotsModal', {$scope: scope}, bindings) 17 | ctrl.$onInit() 18 | })) 19 | 20 | it('calls save when save is called', () => { 21 | const spy = sinon.spy(ctrl, 'save') 22 | ctrl.save() 23 | 24 | expect(spy).to.have.been.called 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /client/app/states/orders/details/details.state.spec.js: -------------------------------------------------------------------------------- 1 | /* global $state, $controller */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('State: orders.details', () => { 4 | beforeEach(function () { 5 | module('app.states', 'app.orders', 'app.core') 6 | }) 7 | 8 | let ctrl 9 | 10 | beforeEach(() => { 11 | bard.inject('$controller', '$state', '$stateParams') 12 | 13 | ctrl = $controller($state.get('orders.details').controller, { 14 | $stateParams: { 15 | serviceOrderId: 213 16 | }, 17 | order: {name: 'test order'}, 18 | serviceTemplate: {name: 'test template'} 19 | }) 20 | }) 21 | 22 | describe('controller', () => { 23 | it('is created successfully', () => { 24 | expect(ctrl).to.exist 25 | }) 26 | 27 | it('has an order title', () => { 28 | expect(ctrl.order.name).to.be.eq('test order') 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /client/app/core/polling.service.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function PollingFactory ($interval, $localStorage, lodash) { 3 | var service = { 4 | start: start, 5 | stop: stop, 6 | stopAll: stopAll, 7 | getPolls: getPolls 8 | } 9 | 10 | var polls = {} 11 | 12 | return service 13 | 14 | function getPolls () { 15 | return polls 16 | } 17 | 18 | function start (key, func, interval, limit) { 19 | var poll 20 | if (angular.isDefined($localStorage.pause)) { 21 | interval = $localStorage.pause 22 | } 23 | if (!polls[key]) { 24 | poll = $interval(func, interval, limit) 25 | polls[key] = poll 26 | } 27 | } 28 | 29 | function stop (key) { 30 | if (polls[key]) { 31 | $interval.cancel(polls[key]) 32 | delete polls[key] 33 | } 34 | } 35 | 36 | function stopAll () { 37 | angular.forEach(lodash.keys(polls), stop) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/app/shared/ss-card/_ss-card.sass: -------------------------------------------------------------------------------- 1 | @import '_bem-support/index' 2 | @import 'app_colors' 3 | 4 | +block(ss-card) 5 | +element(description) 6 | font-size: 12px 7 | font-weight: 700 8 | height: 20px 9 | margin-top: 10px 10 | 11 | +element(logo_wrapper) 12 | height: 155px 13 | padding-top: 10px 14 | 15 | +element(logo) 16 | display: inline-block 17 | left: -2px 18 | max-height: 120px 19 | max-width: 190px 20 | position: relative 21 | 22 | background: $app-color-white 23 | border-top: 2px solid transparent 24 | box-shadow: 0 1px 1px $color-black-20 25 | cursor: pointer 26 | float: left 27 | height: 290px 28 | margin-bottom: 20px 29 | margin-right: 20px 30 | padding: 10px 31 | position: relative 32 | text-align: center 33 | width: 260px 34 | 35 | &:hover 36 | border: 1px solid $app-color-light-gray-5 37 | box-shadow: 0 3px 10px -2px $color-black-30 38 | -------------------------------------------------------------------------------- /client/app/services/usage-graphs/usage-graphs.service.spec.js: -------------------------------------------------------------------------------- 1 | /* global UsageGraphsService */ 2 | describe('Service: UsageGraphsFactory', () => { 3 | let service 4 | beforeEach(module('app.services', 'app.shared')) 5 | beforeEach(() => { 6 | bard.inject('UsageGraphsService') 7 | service = UsageGraphsService 8 | }) 9 | it('should allow you to get a charts config', () => { 10 | const cpuChartConfig = {'units': __('MHz'), 'chartId': 'cpuChart', 'label': __('used')} 11 | const cpuChart = service.getChartConfig(cpuChartConfig, 100, 200) 12 | const expectedConfig = { 13 | 'config': {'units': 'MHz', 'chartId': 'cpuChart'}, 14 | 'data': {'used': 100, 'total': 200}, 15 | 'label': 'used' 16 | } 17 | expect(cpuChart).to.eql(expectedConfig) 18 | }) 19 | it('should convert bytes to gb', () => { 20 | const gb = service.convertBytestoGb(1073741824) 21 | expect(gb).to.eq(1.00) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-xen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-xensource.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /client/app/services/custom-button/_custom-button.sass: -------------------------------------------------------------------------------- 1 | @import 'app_colors' 2 | .custom-button-tooltip 3 | display: inline-block 4 | 5 | .custom-actions-menu 6 | .custom-button-action 7 | border-color: transparent 8 | border-style: solid 9 | border-width: 1px 0 10 | clear: both 11 | color: $app-color-dark-gray-2 12 | display: block 13 | line-height: 1.66666667 14 | padding: 3px 20px 15 | white-space: nowrap 16 | 17 | &:hover 18 | background-color: $app-color-light-blue-5 19 | border-color: $app-color-light-blue-4 20 | color: $app-color-medium-gray-2 21 | text-decoration: none 22 | 23 | .custom-button-action 24 | &.disabled 25 | color: $app-color-light-gray-7 26 | 27 | &:hover 28 | background-color: transparent 29 | border-color: transparent 30 | color: $app-color-light-gray-7 31 | 32 | .toolbar-pf-actions 33 | .dropdown-menu 34 | .disabled 35 | cursor: not-allowed 36 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-citrix.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/assets/sass/styles.sass: -------------------------------------------------------------------------------- 1 | // Global constants 2 | $img-base-path: '/' !default 3 | 4 | @import 'patternfly' 5 | 6 | // Include font-fabulous 7 | $ff-font-path: '~@manageiq/font-fabulous/assets/fonts/font-fabulous' 8 | @import 'font-fabulous' 9 | 10 | // BEM Support : See _bem-support/_index.sass for more information 11 | @import '_bem-support/index' 12 | 13 | // Variables: colors, sizes, ... 14 | @import 'app_colors' 15 | @import 'app_urls' 16 | 17 | // Base styles (application wide styles, ...) 18 | @import 'base' 19 | 20 | // Any other styles 21 | @import 'splash' 22 | @import 'buttons' 23 | @import 'forms' 24 | @import 'details-view' 25 | @import 'list-view' 26 | @import 'breadcrumbs' 27 | 28 | // Include component styles 29 | @import '../../app/components/components' 30 | 31 | // Include layout styles 32 | @import '../../app/layouts/layouts' 33 | 34 | // Include states styles 35 | @import '../../app/states/states' 36 | 37 | // Include over styles 38 | @import 'overrides' 39 | -------------------------------------------------------------------------------- /client/app/states/login/_login.sass: -------------------------------------------------------------------------------- 1 | @import 'app_colors' 2 | 3 | .login-pf 4 | background-color: $app-color-deep-blue 5 | background-image: none 6 | height: calc(100vh) 7 | margin-bottom: -38px 8 | margin-top: -60px 9 | 10 | // override .login-pf #brand img to match ui-classic brand.scss 11 | #brand // sass-lint:disable-line no-ids 12 | img 13 | height: 38px !important // sass-lint:disable-line no-important 14 | 15 | .container 16 | padding-top: 0 17 | 18 | .alert-danger 19 | background-color: $app-color-light-red 20 | border-color: $app-color-dark-red 21 | color: $app-color-dark-gray-2 22 | 23 | h1 24 | margin: 0 25 | 26 | .img-top 27 | left: 0 28 | top: 0 29 | 30 | .img-bottom 31 | bottom: 0 32 | position: fixed 33 | right: 0 34 | 35 | .logo 36 | max-height: 200px 37 | max-width: 200px 38 | 39 | .spinner-wrap 40 | height: 16px 41 | position: absolute 42 | right: 40px 43 | z-index: 1000 44 | -------------------------------------------------------------------------------- /client/app/core/gettext.config.js: -------------------------------------------------------------------------------- 1 | /* eslint no-constant-condition: "off" */ 2 | import languageFile from '../../gettext/json/manageiq-ui-service.json' 3 | /** @ngInject */ 4 | export function gettextInit ($window, gettextCatalog, gettext) { 5 | // prepend [MISSING] to untranslated strings 6 | gettextCatalog.debug = false 7 | gettextCatalog.loadAndSet = function (lang) { 8 | if (lang) { 9 | lang = lang.replace('_', '-') 10 | gettextCatalog.setCurrentLanguage(lang) 11 | if (lang !== 'en') { 12 | gettextCatalog.setStrings(lang, languageFile[lang]) 13 | } 14 | } 15 | } 16 | 17 | $window.N_ = gettext 18 | $window.__ = gettextCatalog.getString.bind(gettextCatalog) 19 | 20 | // 'locale_name' will be translated into locale name in every translation 21 | // For example, in german translation it will be 'Deutsch', in slovak 'Slovensky', etc. 22 | // The localized locale name will then be presented to the user to select from in the UI. 23 | const localeName = __('locale_name') 24 | } 25 | -------------------------------------------------------------------------------- /client/app/core/tag-editor-modal/tag-editor-modal.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | 22 | 31 | -------------------------------------------------------------------------------- /client/app/states/logout/logout.state.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function LogoutState (routerHelper) { 3 | routerHelper.configureStates(getStates()) 4 | } 5 | 6 | function getStates () { 7 | return { 8 | 'logout': { 9 | url: '/logout', 10 | controller: StateController, 11 | controllerAs: 'vm', 12 | title: N_('Logout') 13 | } 14 | } 15 | } 16 | 17 | /** @ngInject */ 18 | function StateController (Session, API_BASE, $window) { 19 | activate() 20 | 21 | function activate () { 22 | var targetLocation 23 | const authMode = Session.getAuthMode() 24 | Session.destroy() 25 | const location = $window.location.href 26 | if (location.includes(`/ui/service`)) { 27 | targetLocation = `/ui/service/` 28 | } else { 29 | targetLocation = `/` 30 | } 31 | if (authMode == 'oidc') { 32 | $window.location.href = '/oidc_login/redirect_uri?logout=' + encodeURI(API_BASE + targetLocation) 33 | } else { 34 | $window.location.href = targetLocation 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-azure.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /client/app/core/modal/base-modal-controller.js: -------------------------------------------------------------------------------- 1 | /** @ngInject */ 2 | export function BaseModalController ($uibModalInstance, $state, CollectionsApi, EventNotifications) { 3 | const vm = this 4 | vm.cancel = cancel 5 | vm.reset = reset 6 | vm.save = save 7 | 8 | function cancel () { 9 | $uibModalInstance.dismiss() 10 | } 11 | 12 | function reset (event) { 13 | angular.copy(event.original, this.modalData) // eslint-disable-line angular/controller-as-vm 14 | } 15 | 16 | function save () { 17 | const vm = this 18 | var data = { 19 | action: vm.action, 20 | resource: vm.modalData 21 | } 22 | 23 | CollectionsApi.post(vm.collection, vm.modalData.id, {}, data).then(saveSuccess, saveFailure) 24 | 25 | function saveSuccess () { 26 | $uibModalInstance.close() 27 | EventNotifications.success(vm.onSuccessMessage) 28 | $state.go($state.current, {}, {reload: true}) 29 | } 30 | 31 | function saveFailure () { 32 | EventNotifications.error(vm.onFailureMessage) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-azure_network.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /client/app/services/detail-reveal/detail-reveal.component.js: -------------------------------------------------------------------------------- 1 | import './_detail-reveal.sass' 2 | import template from './detail-reveal.html'; 3 | 4 | export const DetailRevealComponent = { 5 | controller: ComponentController, 6 | controllerAs: 'vm', 7 | bindings: { 8 | detailTitle: '@', 9 | detail: '@', 10 | icon: '@', 11 | translateTitle: '<', 12 | rowClass: '@', 13 | displayField: ' { 23 | if (angular.isUndefined(vm.displayField)) { 24 | vm.displayField = false 25 | } 26 | 27 | vm.translateTitle = (angular.isUndefined(vm.translateTitle) ? true : vm.translateTitle) 28 | vm.detailTitle = (vm.translateTitle === true ? __(vm.detailTitle) : vm.detailTitle) 29 | vm.rowClass = (angular.isDefined(vm.rowClass) ? vm.rowClass : 'row detail-row') 30 | vm.toggleDetails = false 31 | vm.hasMoreDetails = $transclude().length > 0 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/app/states/login/login.state.spec.js: -------------------------------------------------------------------------------- 1 | /* global $state, EventNotifications, $controller, Session */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('State: login', () => { 4 | beforeEach(() => { 5 | module('app.states') 6 | }) 7 | 8 | describe('controller', () => { 9 | let ctrl 10 | beforeEach(() => { 11 | bard.inject('$controller', '$state', '$stateParams', 'Session', '$window', 'API_LOGIN', 'API_PASSWORD', 'EventNotifications') 12 | sinon.spy(EventNotifications, 'error') 13 | ctrl = $controller($state.get('login').controller, {}) 14 | }) 15 | 16 | describe('controller initialization', () => { 17 | it('is created successfully', () => { 18 | expect(ctrl).to.exist 19 | }) 20 | 21 | it('sets app brand', () => { 22 | expect(ctrl.text.brand).to.equal('ManageIQ Service UI') 23 | }) 24 | 25 | it('sets session privilegesError', () => { 26 | ctrl.onSubmit() 27 | expect(Session.privilegesError).to.be.false 28 | }) 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /client/app/services/edit-service-modal/edit-service-modal.component.js: -------------------------------------------------------------------------------- 1 | import template from './edit-service-modal.html'; 2 | 3 | export const EditServiceModalComponent = { 4 | controller: ComponentController, 5 | controllerAs: 'vm', 6 | bindings: { 7 | resolve: '<', 8 | modalInstance: '<', 9 | close: '&', 10 | dismiss: '&' 11 | }, 12 | template, 13 | } 14 | 15 | /** @ngInject */ 16 | function ComponentController ($controller, sprintf) { 17 | const vm = this 18 | vm.$onInit = function () { 19 | const base = $controller('BaseModalController', { 20 | $uibModalInstance: vm.modalInstance 21 | }) 22 | angular.extend(vm, base) 23 | 24 | vm.modalData = { 25 | id: vm.resolve.service.id, 26 | name: vm.resolve.service.name, 27 | description: vm.resolve.service.description 28 | } 29 | 30 | vm.action = 'edit' 31 | vm.collection = 'services' 32 | vm.onSuccessMessage = sprintf(__('%s was edited.'), vm.resolve.service.name) 33 | vm.onFailureMessage = __('There was an error editing this service.') 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/app/services/detail-reveal/detail-reveal.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 | {{vm.detailTitle}} 7 |
8 |
9 | 10 | {{vm.detail}} 11 |
12 |
13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /client/app/shared/custom-dropdown/custom-dropdown.component.js: -------------------------------------------------------------------------------- 1 | import './_custom-dropdown.sass' 2 | import template from './custom-dropdown.html'; 3 | 4 | export const CustomDropdownComponent = { 5 | controller: ComponentController, 6 | controllerAs: 'vm', 7 | bindings: { 8 | config: '<', 9 | items: '<', 10 | itemsCount: '<', 11 | onUpdate: '&', 12 | menuRight: '@' 13 | }, 14 | template, 15 | } 16 | 17 | /** @ngInject */ 18 | function ComponentController () { 19 | const vm = this 20 | 21 | vm.$onInit = function () { 22 | vm.menuRight = vm.menuRight && (vm.menuRight === 'true' || vm.menuRight === true) 23 | angular.extend(vm, { 24 | handleAction: handleAction, 25 | isOpen: false 26 | }) 27 | } 28 | 29 | vm.$onChanges = function () { 30 | updateDisabled() 31 | } 32 | // Public 33 | 34 | // Private 35 | function updateDisabled () { 36 | vm.onUpdate({$config: vm.config, $changes: vm.items}) 37 | } 38 | 39 | function handleAction (option) { 40 | if (!option.isDisabled) { 41 | option.actionFn(option) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client/app/catalogs/catalog-explorer.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 |
11 | 12 | 17 | 21 | 22 | 23 |
24 | 28 | 29 | -------------------------------------------------------------------------------- /client/app/core/appliance-info.service.js: -------------------------------------------------------------------------------- 1 | import gitHash from '/version.json'; 2 | 3 | /** @ngInject */ 4 | export function ApplianceInfo($sessionStorage) { 5 | return { 6 | get, 7 | set, 8 | }; 9 | 10 | function get() { 11 | return $sessionStorage.applianceInfo || {}; 12 | } 13 | 14 | function set(data) { 15 | $sessionStorage.applianceInfo = { 16 | copyright: data.product_info.copyright, 17 | supportWebsiteText: data.product_info.support_website_text, 18 | supportWebsite: data.product_info.support_website, 19 | user: data.identity.name, 20 | role: data.identity.role, 21 | suiVersion: gitHash && gitHash.gitCommit || '', 22 | miqVersion: data.server_info.version + '.' + data.server_info.build, 23 | server: data.server_info.appliance, 24 | asyncNotify: data.settings.asynchronous_notifications || true, 25 | nameFull: data.product_info.name_full, 26 | brand: data.product_info.branding_info.brand, 27 | favicon: data.product_info.branding_info.favicon, 28 | logo: data.product_info.branding_info.logo, 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/app/services/retire-remove-service-modal/retire-remove-service-modal.html: -------------------------------------------------------------------------------- 1 | 8 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /client/app/core/save-modal-dialog/save-modal-dialog.html: -------------------------------------------------------------------------------- 1 |
2 | 9 | 13 | 18 |
19 | -------------------------------------------------------------------------------- /client/app/components/notifications/_notifications.sass: -------------------------------------------------------------------------------- 1 | .drawer-pf 2 | right: 15px 3 | 4 | .panel-counter 5 | &.sub-heading 6 | font-size: 12px 7 | font-weight: normal 8 | 9 | .drawer-pf-notification 10 | .progress 11 | background-color: $color-white 12 | border-color: $color-mine-shaft-approx 13 | border-radius: 5px 14 | box-shadow: inset 0 0 1px $color-black-1 15 | height: 10px 16 | margin-bottom: 0 17 | 18 | .progress-bar-info 19 | background-color: $color-lochmara-approx 20 | 21 | .mini-progress 22 | .time 23 | left: 40px 24 | position: absolute 25 | width: 65px 26 | 27 | .mini-progress-area 28 | left: 115px 29 | margin-bottom: 5px 30 | margin-top: 5px 31 | position: absolute 32 | right: 55px 33 | 34 | .drawer-pf-action 35 | &.footer-actions 36 | width: 100% 37 | 38 | .footer-button-left 39 | display: inline-block 40 | margin-left: 30px 41 | text-align: left 42 | 43 | .footer-button-right 44 | display: inline-block 45 | float: right 46 | margin-right: 30px 47 | text-align: right 48 | -------------------------------------------------------------------------------- /client/app/services/detail-reveal/_detail-reveal.sass: -------------------------------------------------------------------------------- 1 | @import 'app_colors' 2 | 3 | .detail-row 4 | border-bottom: solid 1px $color-concrete-solid 5 | padding: 5px 0 6 | 7 | &.details-exist 8 | cursor: pointer 9 | 10 | &:hover 11 | background-color: $color-cararra-approx 12 | 13 | &.row-active 14 | background-color: $color-cararra-approx 15 | border: 1px solid $color-blue-300 16 | 17 | .detail-reveal-arrow-container 18 | border-right: 1px solid $color-concrete-solid 19 | display: inline-block 20 | height: 21px 21 | margin-right: 13px 22 | padding-right: 15px 23 | width: 25px 24 | 25 | &.no-details 26 | visibility: hidden 27 | 28 | .detail-reveal-arrow 29 | vertical-align: 0 30 | width: 10px 31 | 32 | &.no-details 33 | color: $color-light-gray-approx 34 | 35 | .item-extra-details-container 36 | border: 1px solid $color-blue-300 37 | border-top: 0 38 | padding-bottom: 10px 39 | 40 | .close-button-container 41 | height: 16px 42 | padding: 12px 43 | 44 | i 45 | cursor: pointer 46 | float: right 47 | 48 | .detail-content 49 | padding-left: 83px 50 | -------------------------------------------------------------------------------- /client/app/services/edit-service-modal/edit-service-modal.html: -------------------------------------------------------------------------------- 1 | 7 | 19 | -------------------------------------------------------------------------------- /client/app/components/dashboard/dashboard.component.spec.js: -------------------------------------------------------------------------------- 1 | /* global $componentController */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('Component: dashboardComponent', () => { 4 | let ctrl 5 | 6 | beforeEach(() => { 7 | module('app.core', 'app.components') 8 | bard.inject('$componentController', 'EventNotifications', '$state', 'DashboardService', 'lodash', 'Chargeback', 'RBAC', 'Polling') 9 | ctrl = $componentController('dashboardComponent', {}, {}) 10 | ctrl.$onInit() 11 | }) 12 | 13 | describe('with $componentController', () => { 14 | it('is defined', () => { 15 | expect(ctrl).to.exist 16 | }) 17 | 18 | it('initializes servicesCounts', () => { 19 | let servicesCount = { 20 | total: 0, 21 | current: 0, 22 | retired: 0, 23 | soon: 0 24 | } 25 | 26 | expect(ctrl.servicesCount).to.eql(servicesCount) 27 | }) 28 | 29 | it('initializes requestsCounts', () => { 30 | let requestsCount = { 31 | total: 0, 32 | pending: 0, 33 | approved: 0, 34 | denied: 0 35 | } 36 | 37 | expect(ctrl.requestsCount).to.eql(requestsCount) 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /client/app/services/retire-service-modal/retire-service-modal.spec.js: -------------------------------------------------------------------------------- 1 | /* global inject */ 2 | describe('Component: RetireServiceModalComponent', () => { 3 | beforeEach(module('app.states', 'app.services')) 4 | 5 | describe('controller', () => { 6 | let $componentController, $scope, ctrl 7 | 8 | beforeEach(inject(function ($rootScope, $injector, _$componentController_) { 9 | $scope = $rootScope.$new() 10 | const bindings = {resolve: {services: []}} 11 | const locals = { 12 | $scope: $scope, 13 | services: angular.noop, 14 | $uibModalInstance: angular.noop, 15 | CollectionsApi: angular.noop, 16 | EventNotifications: angular.noop 17 | } 18 | 19 | $componentController = _$componentController_ 20 | ctrl = $componentController('retireServiceModal', locals, bindings) 21 | })) 22 | 23 | it('changes visible options when date is changed', () => { 24 | ctrl.$onInit() 25 | expect(ctrl.visibleOptions.length).to.eq(0) 26 | 27 | // Initial digest populates visibleOptions with 'No warning' option 28 | $scope.$digest() 29 | 30 | expect(ctrl.visibleOptions.length).to.eq(1) 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /client/app/shared/confirmation/_confirmation.sass: -------------------------------------------------------------------------------- 1 | @import '_bem-support/index' 2 | @import 'app_colors' 3 | 4 | +block(confirmation) 5 | 6 | +element(items-content) 7 | position: relative 8 | 9 | .items-content-title 10 | color: $color-black 11 | padding-left: 20px 12 | padding-right: 5px 13 | text-decoration: none 14 | transition: 250ms 15 | 16 | &.collapsable 17 | color: $app-color-medium-blue-2 18 | cursor: pointer 19 | padding-left: 15px 20 | 21 | &:hover 22 | color: $color-medium-blue-3 23 | 24 | &::before 25 | content: '\f107' 26 | display: block 27 | font-family: FontAwesome 28 | left: 0 29 | position: absolute 30 | top: 0 31 | 32 | &.collapsed 33 | &::before 34 | content: '\f105' 35 | 36 | .items-list 37 | list-style: none 38 | margin-top: 5px 39 | max-height: 200px 40 | overflow-y: auto 41 | padding-left: 15px 42 | 43 | +element(dialog) 44 | margin: 0 45 | position: absolute 46 | width: 300px 47 | 48 | +element(body) 49 | padding: 16px 50 | text-align: center 51 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches-ignore: 6 | - dependabot/* 7 | - renovate/* 8 | schedule: 9 | - cron: 0 0 * * * 10 | workflow_dispatch: 11 | concurrency: 12 | group: "${{ github.workflow }}-${{ github.ref }}" 13 | cancel-in-progress: true 14 | permissions: 15 | contents: read 16 | jobs: 17 | ci: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | test-suite: 22 | - test 23 | - test:security 24 | steps: 25 | - uses: actions/checkout@v6 26 | - name: Set up system 27 | run: bin/before_install 28 | - name: Set up Node 29 | uses: actions/setup-node@v6 30 | with: 31 | node-version-file: package.json 32 | cache: "${{ !env.ACT && 'yarn' || '' }}" 33 | registry-url: https://npm.manageiq.org/ 34 | - name: Prepare tests 35 | run: bin/setup 36 | - name: Run tests 37 | run: yarn run ${{ matrix.test-suite }} 38 | - name: Report code coverage 39 | if: "${{ github.ref == 'refs/heads/master' && matrix.test-suite != 'test:security' }}" 40 | continue-on-error: true 41 | run: cat reports/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 42 | -------------------------------------------------------------------------------- /client/app/shared/format-bytes.filter.spec.js: -------------------------------------------------------------------------------- 1 | /* global inject */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('Filter: format-bytes/format-megaBytes', () => { 4 | let formatBytesFilter 5 | let formatmegaBytesFilter 6 | 7 | // load the module 8 | beforeEach(module('app.shared')) 9 | 10 | // load filter function into variable 11 | beforeEach(inject(($filter) => { 12 | formatBytesFilter = $filter('formatBytes') 13 | formatmegaBytesFilter = $filter('megaBytes') 14 | })) 15 | 16 | it('should exist when invoked', () => { 17 | expect(formatBytesFilter).to.exist 18 | expect(formatmegaBytesFilter).to.exist 19 | }) 20 | 21 | it('should correctly display valid format', () => { 22 | expect(formatBytesFilter(2048)).to.be.eq('2 kB') 23 | expect(formatmegaBytesFilter(128)).to.be.eq(134217728) 24 | }) 25 | 26 | it('should correctly display valid format', () => { 27 | expect(formatBytesFilter(42424242424242)).to.be.eq('38.58 TB') 28 | }) 29 | 30 | it('should display hyphen when NAN', () => { 31 | expect(formatBytesFilter('foo')).to.be.eq('-') 32 | }) 33 | 34 | it('should correctly display invalid format', () => { 35 | expect(formatBytesFilter(0)).to.be.eq('0 Bytes') 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /client/app/core/rbac.service.js: -------------------------------------------------------------------------------- 1 | import constants from './product-features.constants.json' 2 | const { productFeatures } = constants; 3 | 4 | /** @ngInject */ 5 | export function RBACFactory (lodash) { 6 | let features = {} 7 | let currentRole 8 | 9 | return { 10 | all: all, 11 | set: set, 12 | has: has, 13 | FEATURES: productFeatures, 14 | hasAny: hasAny, 15 | hasRole: hasRole, 16 | setRole: setRole, 17 | suiAuthorized: suiAuthorized 18 | } 19 | 20 | function set (productFeatures) { 21 | features = productFeatures || {} 22 | } 23 | 24 | function has (feature) { 25 | return feature in features 26 | } 27 | 28 | function hasAny (permissions) { 29 | return permissions.some((feature) => angular.isDefined(features[feature])) 30 | } 31 | 32 | function hasRole (...roles) { 33 | return roles.some((role) => role === currentRole || role === '_ALL_') 34 | } 35 | 36 | function all () { 37 | return features 38 | } 39 | 40 | function setRole (newRole) { 41 | currentRole = newRole 42 | } 43 | 44 | function suiAuthorized () { 45 | return hasAny([productFeatures.SERVICES.VIEW, 46 | productFeatures.ORDERS.VIEW, 47 | productFeatures.SERVICE_CATALOG.VIEW]) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /client/assets/images/os/os-linux_esx.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/assets/images/os/os-esx-server-3i.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/assets/images/os/os-vmware-esx-server.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-vmware.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/app/components/notifications/notification-footer.html: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-vmwarews.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-vmware_cloud.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-vmware_cloud_network.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/app/shared/custom-dropdown/custom-dropdown.component.spec.js: -------------------------------------------------------------------------------- 1 | /* global $componentController */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('Component: customDropdown ', function () { 4 | let ctrl 5 | let updateSpy 6 | 7 | beforeEach(function () { 8 | module('app.core', 'app.shared') 9 | bard.inject('$componentController') 10 | ctrl = $componentController('customDropdown', {}, { 11 | config: { 12 | 'test': 'test' 13 | }, 14 | items: ['item1', 'item2'], 15 | onUpdate: function () {}, 16 | menuRight: true 17 | }) 18 | updateSpy = sinon.stub(ctrl, 'onUpdate').returns(true) 19 | }) 20 | 21 | it('is defined', function () { 22 | expect(ctrl).to.exist 23 | }) 24 | 25 | it('should handle changes', () => { 26 | ctrl.$onChanges() 27 | expect(updateSpy).have.been.calledWith( 28 | {$changes: ['item1', 'item2'], $config: {test: 'test'}}) 29 | }) 30 | 31 | it('should handle actions', () => { 32 | const options = { 33 | isDisabled: false, 34 | actionFn: function () {} 35 | } 36 | const actionFnSpy = sinon.stub(options, 'actionFn').returns(true) 37 | ctrl.$onInit() 38 | ctrl.handleAction(options) 39 | expect(actionFnSpy).to.have.been.called 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /client/app/core/site-switcher/site-switcher.component.spec.js: -------------------------------------------------------------------------------- 1 | /* global $rootScope, $compile */ 2 | describe('SiteSwitcher test', () => { 3 | let scope, compile, compiledElement; 4 | 5 | const sites = [{ 6 | title: 'Launch Operations UI', 7 | tooltip: 'Launch Operations UI', 8 | iconClass: 'fa-cogs', 9 | url: 'http://www.google.com' 10 | }, { 11 | title: 'Launch Service UI', 12 | tooltip: 'Launch Service UI', 13 | iconClass: 'fa-cog', 14 | url: 'http://www.cnn.com' 15 | }, { 16 | title: 'Home', 17 | tooltip: 'Home', 18 | iconClass: 'fa-home', 19 | url: 'http://www.redhat.com' 20 | }]; 21 | 22 | beforeEach(() => { 23 | module('app.core'); 24 | bard.inject('$rootScope', '$compile'); 25 | 26 | scope = $rootScope.$new(); 27 | compile = $compile; 28 | 29 | scope.sites = sites; 30 | 31 | const element = angular.element(''); 32 | compiledElement = compile(element)(scope); 33 | 34 | scope.$digest(); 35 | }); 36 | 37 | it('creates site switcher with embedded hrefs', () => { 38 | let header = compiledElement[0].querySelector('.miq-siteswitcher'); 39 | expect(header).to.not.be.empty 40 | expect(header.querySelectorAll('.miq-siteswitcher-icon').length).to.be.eq(1); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /client/app/orders/process-order-modal/process-order-modal.component.js: -------------------------------------------------------------------------------- 1 | import template from './process-order-modal.html'; 2 | 3 | export const ProcessOrderModalComponent = { 4 | controller: ComponentController, 5 | controllerAs: 'vm', 6 | bindings: { 7 | resolve: '<', 8 | close: '&', 9 | dismiss: '&' 10 | }, 11 | template, 12 | } 13 | 14 | /** @ngInject */ 15 | function ComponentController ($state, CollectionsApi, EventNotifications) { 16 | const vm = this 17 | 18 | vm.$onInit = () => { 19 | angular.extend(vm, { 20 | order: vm.resolve.order, 21 | confirm: confirm, 22 | cancel: cancel 23 | }) 24 | } 25 | 26 | function cancel () { 27 | vm.dismiss({$value: 'cancel'}) 28 | } 29 | 30 | function confirm () { 31 | var data = { 32 | action: 'delete', 33 | resources: [vm.order] 34 | } 35 | CollectionsApi.post('service_orders', '', {}, data).then(saveSuccess, saveFailure) 36 | 37 | function saveSuccess (response) { 38 | vm.close() 39 | EventNotifications.batch(response.results, __('Deleting order.'), __('Error deleting order.')) 40 | $state.go($state.current, {}, {reload: true}) 41 | } 42 | 43 | function saveFailure () { 44 | EventNotifications.error(__('There was an error removing order')) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/app/services/generic-objects-list/generic-objects-list.component.js: -------------------------------------------------------------------------------- 1 | import template from './generic-objects-list.html'; 2 | import './_generic_objects_list.sass' 3 | 4 | export const GenericObjectsListComponent = { 5 | controller: ComponentController, 6 | controllerAs: 'vm', 7 | bindings: { 8 | genericObject: '<', 9 | onUpdate: '&' 10 | }, 11 | template, 12 | } 13 | 14 | /** @ngInject */ 15 | function ComponentController (EventNotifications, CollectionsApi) { 16 | const vm = this 17 | 18 | vm.$onInit = () => { 19 | angular.extend(vm, { 20 | genericObjectsListConfig: { 21 | showSelectBox: false, 22 | useExpandingRows: true, 23 | compoundExpansionOnly: true 24 | }, 25 | customActionClick: customActionClick, 26 | toggleExpand: toggleExpand 27 | }) 28 | } 29 | function toggleExpand (item) { 30 | item.isExpanded = !item.isExpanded 31 | vm.onUpdate({object: item}) 32 | } 33 | function customActionClick (item, action) { 34 | const data = { 35 | action: action 36 | } 37 | CollectionsApi.post('generic_objects', item.id, {}, data).then((response) => { 38 | EventNotifications.success(response.message) 39 | }).catch((response) => { 40 | EventNotifications.error(__('An error occured')) 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/app/shared/tagging/tagging.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 8 | 12 | 13 |
14 | 15 |
There are no tags for this item.
16 | 17 |
    18 |
  • 19 | 20 | {{tag.categorization.displayName}} 21 | 22 | 23 |
  • 24 |
25 | 26 | -------------------------------------------------------------------------------- /client/app/core/site-switcher/site-switcher.component.js: -------------------------------------------------------------------------------- 1 | import './_site-switcher.sass'; 2 | import template from './site-switcher.html'; 3 | 4 | /** 5 | * @description 6 | * Component for showing a site switcher drop down for moving between different UI's. 7 | * Settings object example: 8 | * ```javascript 9 | * { 10 | * sites: [{ 11 | * title: 'Launch Operations UI', 12 | * tooltip: 'Launch Operations UI', 13 | * iconClass: 'fa-cogs', 14 | * url: 'http://www.manageiq.com' 15 | * }, { 16 | * title: 'Launch Service UI', 17 | * tooltip: 'Launch Service UI', 18 | * iconClass: 'fa-cog', 19 | * url: 'http://www.manageiq.com' 20 | * }, { 21 | * title: 'Home', 22 | * tooltip: 'Home', 23 | * iconClass: 'fa-home', 24 | * url: 'http://www.manageiq.com' 25 | * }] 26 | * } 27 | * ``` 28 | * @memberof miqStaticAssets 29 | * @ngdoc component 30 | * @name miqSiteSwitcher 31 | * @attr {Array} sites 32 | * An array of sites to display in the switcher, tooltip is optional 33 | * 34 | * @example 35 | * 36 | * 37 | */ 38 | export const SiteSwitcher = { 39 | template, 40 | controllerAs: 'ctrl', 41 | bindings: { 42 | sites: '<', 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /client/app/orders/order-explorer/order-explorer.component.spec.js: -------------------------------------------------------------------------------- 1 | /* global inject, $state */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('Component: orderExplorer', () => { 4 | beforeEach(() => { 5 | module('app.states', 'app.orders') 6 | }) 7 | 8 | describe('with $componentController', () => { 9 | let scope 10 | let ctrl 11 | 12 | beforeEach(inject(function ($componentController) { 13 | ctrl = $componentController('orderExplorer', {$scope: scope}, {}) 14 | ctrl.$onInit() 15 | })) 16 | 17 | it('is defined', () => { 18 | expect(ctrl).to.exist 19 | }) 20 | 21 | it('should work with $state.go', () => { 22 | bard.inject('$state') 23 | 24 | $state.go('orders') 25 | expect($state.is('orders.explorer')) 26 | }) 27 | 28 | it('is can report back a request state', () => { 29 | const item = { 30 | request_state: 'finished', 31 | status: 'Ok' 32 | } 33 | const status = ctrl.requestStatus(item) 34 | expect(status).to.eq('finished') 35 | }) 36 | it('is can report back a failed order status', () => { 37 | const item = { 38 | request_state: 'finished', 39 | status: 'Error' 40 | } 41 | const status = ctrl.requestStatus(item) 42 | expect(status).to.eq('Error') 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-openshift.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /client/assets/images/os/os-linux_coreos.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-openshift_enterprise.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /client/app/core/exception/exception.service.js: -------------------------------------------------------------------------------- 1 | // The exception service is used to easily create appropriate callbacks to pass 2 | // to `.catch` clauses of Promise chains. 3 | // 4 | // If you simply want to log the exception and reject so that another `.catch` 5 | // can handle, use `exception.log` and pass a log message: 6 | // 7 | // .catch(exception.log('Something went wrong')); 8 | // 9 | // If you are using `.catch` in a place where you wish to handle the exception 10 | // and not reject use `exception.catch`: 11 | // 12 | // .catch(exception.catch('Nothing to do here')); 13 | 14 | /** @ngInject */ 15 | export function exception ($log) { 16 | var service = { 17 | catch: exceptionCatcher, 18 | log: exceptionLogger 19 | } 20 | 21 | return service 22 | 23 | function exceptionCatcher (message) { 24 | return function (error) { 25 | logErrorMessage(message, error) 26 | } 27 | } 28 | 29 | function exceptionLogger (message) { 30 | return function (error) { 31 | logErrorMessage(message, error) 32 | 33 | return Promise.reject(error) 34 | } 35 | } 36 | 37 | // Private 38 | function logErrorMessage (message, error) { 39 | if (error.data && error.data.description) { 40 | message += '\n' + error.data.description 41 | error.data.description = message 42 | } 43 | 44 | $log.error(message, error) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/app/shared/pagination/pagination.component.spec.js: -------------------------------------------------------------------------------- 1 | /* global inject */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('Component: pagination', () => { 4 | beforeEach(module('app.components')) 5 | 6 | describe('controller', () => { 7 | let $componentController, ctrl 8 | const bindings = {limit: 5, count: 11, offset: 0, onUpdate: angular.noop} 9 | 10 | beforeEach(inject(function (_$componentController_) { 11 | $componentController = _$componentController_ 12 | ctrl = $componentController('explorerPagination', null, bindings) 13 | })) 14 | 15 | it('is defined, accepts bindings limit/count/offset', () => { 16 | expect(ctrl).to.exist 17 | expect(ctrl.limit).to.equal(5) 18 | expect(ctrl.count).to.equal(11) 19 | expect(ctrl.offset).to.equal(0) 20 | }) 21 | 22 | it('next increments offset by limit', () => { 23 | ctrl.$onInit() 24 | ctrl.next() 25 | expect(ctrl.offset).to.equal(5) 26 | expect(ctrl.rightBoundary).to.equal(10) 27 | expect(ctrl.leftBoundary).to.equal(6) 28 | }) 29 | 30 | it('previous decrements offset by limit', () => { 31 | ctrl.$onInit() 32 | ctrl.next() 33 | ctrl.previous() 34 | expect(ctrl.offset).to.equal(0) 35 | expect(ctrl.rightBoundary).to.equal(5) 36 | expect(ctrl.leftBoundary).to.equal(1) 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /client/assets/sass/_explorer.sass: -------------------------------------------------------------------------------- 1 | .explorer-list 2 | .list-group-item 3 | &.list-view-pf-expand-active 4 | padding-bottom: 0 5 | 6 | .power-icon 7 | cursor: default 8 | 9 | .list-view-pf 10 | height: calc(100vh - 237px) 11 | overflow-y: auto 12 | 13 | .explorer-children-list 14 | .list-view-pf 15 | height: auto 16 | overflow-y: auto 17 | 18 | [class*='col-'] 19 | padding-left: 10px 20 | padding-right: 10px 21 | 22 | .name-column 23 | .pficon 24 | display: inline 25 | margin-left: 5px 26 | 27 | .list-view-stacked-item 28 | line-height: 24px 29 | 30 | .fa, 31 | .pficon 32 | text-align: center 33 | 34 | .dropdown-kebab-pf 35 | .dropdown-menu 36 | &.dropdown-menu-right 37 | &::after, 38 | &::before 39 | right: 18px 40 | 41 | .explorer-children-list 42 | 43 | .list-group-item-header 44 | cursor: default 45 | margin-left: 9px 46 | width: calc(100% - 46px) 47 | 48 | .list-view-pf 49 | padding-bottom: 0 50 | 51 | .list-group 52 | border-top: 0 53 | margin-bottom: 0 54 | 55 | .dropdown-kebab-pf 56 | position: absolute 57 | right: 10px 58 | 59 | .btn-link 60 | padding: 4px 0 61 | 62 | .card-view-pf 63 | padding-bottom: 40px 64 | 65 | .card 66 | cursor: pointer 67 | 68 | .service-explorer-row 69 | cursor: pointer 70 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | amd: true, 4 | browser: true, 5 | es6: true, 6 | jasmine: true, 7 | jquery: true, 8 | node: true, 9 | protractor: true, 10 | }, 11 | extends: [ 12 | 'eslint:recommended', 13 | 'eslint-config-angular', 14 | ], 15 | globals: { 16 | Atomics: 'readonly', 17 | N_: 'readonly', 18 | SharedArrayBuffer: 'readonly', 19 | __: 'readonly', 20 | angular: 'readonly', 21 | bard: 'readonly', 22 | sinon: 'readonly', 23 | }, 24 | parser: 'babel-eslint', 25 | parserOptions: { 26 | ecmaVersion: 2018, 27 | sourceType: 'module', 28 | }, 29 | plugins: [ 30 | 'eslint-plugin-angular', 31 | 'eslint-plugin-import', 32 | ], 33 | rules: { 34 | // prevent eslint-plugin-angular warnings about behaviour change 35 | 'angular/service-name': ['error', { 36 | oldBehavior: false, 37 | }], 38 | 39 | // disable a bunch of "use the angular version of this" warnings 40 | 'angular/angularelement': 'off', 41 | 'angular/definedundefined': 'off', 42 | 'angular/document-service': 'off', 43 | 'angular/json-functions': 'off', 44 | 'angular/log': 'off', 45 | 'angular/on-watch': 'off', 46 | 'angular/typecheck-array': 'off', 47 | 'angular/typecheck-object': 'off', 48 | 'angular/window-service': 'off', 49 | 50 | // FIXME: 71 problems 51 | 'no-unused-vars': 'off', 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /client/app/core/save-modal-dialog/save-modal-dialog.factory.js: -------------------------------------------------------------------------------- 1 | import template from './save-modal-dialog.html'; 2 | 3 | /** @ngInject */ 4 | export function SaveModalDialogFactory ($uibModal) { 5 | var modalSaveDialog = { 6 | showModal: showModal 7 | } 8 | 9 | return modalSaveDialog 10 | 11 | function showModal (saveCallback, cancelCallback, okToSave) { 12 | var modalOptions = { 13 | template, 14 | controller: SaveModalDialogController, 15 | controllerAs: 'vm', 16 | resolve: { 17 | saveCallback: resolveSave, 18 | cancelCallback: resolveCancel, 19 | okToSave: resolveOkToSave 20 | } 21 | } 22 | 23 | function resolveSave () { 24 | return saveCallback 25 | } 26 | 27 | function resolveCancel () { 28 | return cancelCallback 29 | } 30 | 31 | function resolveOkToSave () { 32 | return okToSave 33 | } 34 | 35 | var modal = $uibModal.open(modalOptions) 36 | 37 | return modal.result 38 | } 39 | } 40 | 41 | /** @ngInject */ 42 | function SaveModalDialogController (saveCallback, cancelCallback, okToSave, $uibModalInstance) { 43 | const vm = this 44 | vm.save = save 45 | vm.cancel = cancel 46 | vm.okToSave = okToSave 47 | 48 | function save () { 49 | saveCallback() 50 | $uibModalInstance.close() 51 | } 52 | 53 | function cancel () { 54 | cancelCallback() 55 | $uibModalInstance.close() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /client/app/services/usage-graphs/usage-graphs.component.js: -------------------------------------------------------------------------------- 1 | import './_usage-graphs.sass' 2 | import template from './usage-graphs.html'; 3 | 4 | export const UsageGraphsComponent = { 5 | bindings: { 6 | cpuChart: '<', 7 | memoryChart: '<', 8 | storageChart: '<', 9 | titleDetails: '@?' 10 | }, 11 | controller: ComponentController, 12 | controllerAs: 'vm', 13 | template, 14 | } 15 | 16 | /** @ngInject */ 17 | function ComponentController () { 18 | const vm = this 19 | vm.$onChanges = () => { 20 | angular.extend(vm, { 21 | cpuChart: vm.cpuChart || {data: {total: 0}}, 22 | memoryChart: vm.memoryChart || {data: {total: 0}}, 23 | storageChart: vm.storageChart || {data: {total: 0}}, 24 | cpuDataExists: false, 25 | memoryDataExists: false, 26 | storageDataExists: false, 27 | emptyState: {icon: 'pficon pficon-help', title: __('No data available')} 28 | }) 29 | 30 | if (vm.cpuChart.data.total > 0) { 31 | vm.cpuDataExists = true 32 | vm.availableCPU = vm.cpuChart.data.total - vm.cpuChart.data.used 33 | } 34 | 35 | if (vm.memoryChart.data.total > 0) { 36 | vm.memoryDataExists = true 37 | vm.availableMemory = vm.memoryChart.data.total - vm.memoryChart.data.used 38 | } 39 | 40 | if (vm.storageChart.data.total > 0) { 41 | vm.storageDataExists = true 42 | vm.availableStorage = vm.storageChart.data.total - vm.storageChart.data.used 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /client/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Service UI 5 | 6 | 7 | 9 | 11 | 12 | 13 | 14 | 28 | 29 | 30 | 33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /client/app/core/shopping-cart/shopping-cart.component.js: -------------------------------------------------------------------------------- 1 | import './_shopping-cart.sass' 2 | import template from './shopping-cart.html'; 3 | 4 | export const ShoppingCartComponent = { 5 | controller: ComponentController, 6 | controllerAs: 'vm', 7 | bindings: { 8 | modalInstance: ' 2 | 3 | 7 | 9 | 11 | 13 | 15 | 17 | 18 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-ec2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 9 | 11 | 13 | 15 | 17 | 18 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-ec2_network.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 9 | 11 | 13 | 15 | 17 | 18 | -------------------------------------------------------------------------------- /config/available-languages.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | 'use strict' 4 | 5 | const glob = require('glob') 6 | const fs = require('fs') 7 | 8 | task() 9 | 10 | function task () { 11 | const config = { 12 | catalogs: '../client/gettext/json/manageiq-ui-service.json', 13 | availLangsFile: '../client/gettext/json/available_languages.json', 14 | supportedLangsFile: '../client/gettext/json/supported_languages.json' 15 | } 16 | 17 | const langFile = glob.sync(config.catalogs) 18 | let availableLanguages = {} 19 | let supportedLanguages = [] 20 | 21 | if (fs.existsSync(config.supportedLangsFile)) { 22 | supportedLanguages = JSON.parse(fs.readFileSync(config.supportedLangsFile, 'utf8')) 23 | } 24 | 25 | const catalog = JSON.parse(fs.readFileSync(langFile[0], 'utf8')) 26 | 27 | for (const propName in catalog) { 28 | if (typeof (catalog[propName]) !== 'undefined') { 29 | // If we have a list of supported languages and the language is not on the list, we skip it 30 | if (supportedLanguages.length > 0 && !supportedLanguages.includes(propName)) { 31 | continue 32 | } 33 | 34 | if (typeof (catalog[propName].locale_name) !== 'undefined') { 35 | availableLanguages[propName] = catalog[propName].locale_name 36 | } else { 37 | availableLanguages[propName] = '' 38 | } 39 | } 40 | } 41 | 42 | availableLanguages = JSON.stringify(availableLanguages) 43 | fs.writeFileSync(config.availLangsFile, availableLanguages, {encoding: 'utf-8', flag: 'w+'}) 44 | } 45 | -------------------------------------------------------------------------------- /client/app/shared/confirmation/confirmation.html: -------------------------------------------------------------------------------- 1 |
2 | 8 | 24 | 34 |
35 | -------------------------------------------------------------------------------- /client/app/components/dashboard/dashboard.component.service.spec.js: -------------------------------------------------------------------------------- 1 | /* global , CollectionsApi, DashboardService */ 2 | describe('Service: DashboardService', () => { 3 | let clock 4 | 5 | beforeEach(() => { 6 | module('app.core', 'app.components') 7 | bard.inject('DashboardService', 'CollectionsApi') 8 | clock = sinon.useFakeTimers(new Date('2016-01-01').getTime()) 9 | }) 10 | const successResponse = { 11 | message: 'Success' 12 | } 13 | afterEach(() => { clock.restore() }) 14 | 15 | it('should query for services', () => { 16 | let d = new Date() 17 | d.setMinutes(d.getMinutes() + 30) 18 | d = d.toISOString() 19 | d = d.substring(0, d.indexOf('.')) 20 | let collectionsApiSpy = sinon.stub(CollectionsApi, 'query').returns(Promise.resolve(successResponse)) 21 | DashboardService.allServices() 22 | expect(collectionsApiSpy.getCall(2).args).to.eql(['services', { 23 | hide: 'resources', 24 | filter: [ 25 | 'retired=false', 26 | 'retires_on>2016-01-01T00:00:00.000Z', 27 | 'retires_on<2016-01-31T00:00:00.000Z', 28 | 'visible=true' 29 | ] 30 | }] 31 | ) 32 | }) 33 | 34 | it('should query for requests', () => { 35 | let collectionsApiSpy = sinon.stub(CollectionsApi, 'query').returns(Promise.resolve(successResponse)) 36 | DashboardService.allRequests() 37 | expect(collectionsApiSpy.getCall(3).args).to.eql(['requests', { 38 | filter: ['type=ServiceReconfigureRequest', 'approval_state=approved'], 39 | hide: 'resources' 40 | } 41 | ]) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /client/app/shared/timeline/timeline.component.spec.js: -------------------------------------------------------------------------------- 1 | /* global inject */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('Component: timeline', function () { 4 | beforeEach(module('app.services')) 5 | describe('controller', function () { 6 | let element = angular.element('') 7 | let ctrl 8 | const bindings = { 9 | data: [{name: 'test', 'data': [{'date': new Date(0), 'details': {'event': 'test', 'object': 'test'}}]}], 10 | options: {width: 900} 11 | } 12 | 13 | beforeEach(inject(function ($componentController) { 14 | ctrl = $componentController('timeline', {$element: element}, bindings) 15 | })) 16 | 17 | it('is defined, accepts bindings data/options', function () { 18 | expect(ctrl).to.exist 19 | expect(ctrl.data).to.exist 20 | expect(ctrl.options.width).to.exist 21 | }) 22 | }) 23 | 24 | describe('template', () => { 25 | let parentScope, $compile 26 | 27 | beforeEach(inject(function (_$compile_, _$rootScope_) { 28 | $compile = _$compile_ 29 | parentScope = _$rootScope_.$new() 30 | })) 31 | 32 | const compileHtml = function (markup, scope) { 33 | let element = angular.element(markup) 34 | $compile(element)(scope) 35 | scope.$digest() 36 | return element 37 | } 38 | 39 | it('should compile timeline when invoked', () => { 40 | const renderedElement = compileHtml(``, parentScope) 41 | expect(renderedElement[0].querySelectorAll('.timeline').length).to.eq(1) 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /client/app/core/rbac.service.spec.js: -------------------------------------------------------------------------------- 1 | /* global readJSON, inject */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('RBAC service', () => { 4 | let service 5 | const permissions = readJSON('tests/mock/rbac/allPermissions.json') 6 | beforeEach(module('app.core')) 7 | 8 | beforeEach(inject((RBAC) => { 9 | service = RBAC 10 | })) 11 | 12 | describe('#hasRole', () => { 13 | it('returns true if given role is _ALL_', () => { 14 | const result = service.hasRole('_ALL_') 15 | 16 | expect(result).to.be.true 17 | }) 18 | 19 | it('returns true if current role matches the given role', () => { 20 | service.setRole('admin') 21 | 22 | const result = service.hasRole('admin') 23 | 24 | expect(result).to.be.true 25 | }) 26 | 27 | it('returns true if current role matches any given role', () => { 28 | service.setRole('super_admin') 29 | 30 | const result = service.hasRole('admin', 'super_admin') 31 | 32 | expect(result).to.be.true 33 | }) 34 | 35 | it('returns false if current role does not match any given roles', () => { 36 | const result = service.hasRole('admin') 37 | 38 | expect(result).to.be.false 39 | }) 40 | 41 | it('returns all feature permissions', () => { 42 | const results = service.all() 43 | expect(results).to.be.empty 44 | }) 45 | 46 | it('allows permissions to be set and retrieved', () => { 47 | service.set(permissions) 48 | const usersPermissions = service.all() 49 | expect(usersPermissions).to.not.be.empty 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /client/app/services/service-details/_service-details.sass: -------------------------------------------------------------------------------- 1 | @import '_bem-support/index' 2 | @import 'app_colors' 3 | 4 | +block(heading-label) 5 | +element(success) 6 | color: $app-color-green 7 | 8 | font-size: 12px 9 | margin: 0 10px 10 | 11 | .service-details-tag-control 12 | padding: 2px 6px 13 | width: 100% 14 | 15 | .no-tag-header 16 | padding-top: 0 17 | 18 | .service-details-resources 19 | border: 0 20 | margin-bottom: 0 21 | 22 | .service-details-resource-group-item 23 | background-color: transparent 24 | 25 | > a 26 | color: $color-black-1 27 | cursor: pointer 28 | font-size: 13px 29 | padding-left: 25px 30 | padding-right: 5px 31 | text-decoration: none 32 | transition: 250ms 33 | 34 | &::before 35 | content: '\f107' 36 | display: block 37 | font-family: FontAwesome 38 | font-size: 16px 39 | font-weight: 500 40 | left: 20px 41 | position: absolute 42 | top: 7px 43 | 44 | &.collapsed 45 | &::before 46 | content: '\f105' 47 | 48 | .service-details-resource-empty-message 49 | opacity: .5 50 | 51 | .service-details-resource-list-container 52 | .list-view-pf 53 | border: 1px solid $color-off-white 54 | margin: 0 55 | padding-bottom: 0 56 | 57 | .list-group-item-header 58 | cursor: default 59 | 60 | .list-view-stacked-item 61 | line-height: 24px 62 | 63 | .usage-graphs-container 64 | background-color: $color-white 65 | padding: 13px 66 | 67 | .usage-graphs-button 68 | i 69 | padding-bottom: 2px 70 | -------------------------------------------------------------------------------- /client/app/services/process-snapshots-modal/process-snapshots-modal.html: -------------------------------------------------------------------------------- 1 | 7 | 22 | 32 | -------------------------------------------------------------------------------- /client/app/states/services/custom_button_details/custom_button_details.html: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 | 27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /client/app/core/list-view.service.js: -------------------------------------------------------------------------------- 1 | /* eslint camelcase: "off" */ 2 | /* eslint no-cond-assign: "off" */ 3 | 4 | /** @ngInject */ 5 | export function ListViewFactory () { 6 | var listView = {} 7 | 8 | listView.applyFilters = function (filters, retList, origList, stateFactory, matchesFilter) { 9 | retList = [] 10 | if (filters && filters.length > 0) { 11 | angular.forEach(origList, filterChecker) 12 | } else { 13 | retList = origList 14 | } 15 | 16 | /* Keep track of the current filtering state */ 17 | stateFactory.setFilters(filters) 18 | 19 | return retList 20 | 21 | function filterChecker (item) { 22 | if (matchesFilters(item, filters)) { 23 | retList.push(item) 24 | } 25 | } 26 | 27 | function matchesFilters (item, filters) { 28 | var matches = true 29 | angular.forEach(filters, filterMatcher) 30 | 31 | function filterMatcher (filter) { 32 | if (!matchesFilter(item, filter)) { 33 | matches = false 34 | 35 | return false 36 | } 37 | } 38 | 39 | return matches 40 | } 41 | } 42 | 43 | listView.createFilterField = function (id, title, placeholder, type, values) { 44 | return { 45 | id: id, 46 | title: title, 47 | placeholder: placeholder, 48 | filterType: type, 49 | filterValues: values 50 | } 51 | } 52 | 53 | listView.createGenericField = function (options) { 54 | return options 55 | } 56 | 57 | listView.createSortField = function (id, title, sortType) { 58 | return { 59 | id: id, 60 | title: title, 61 | sortType: sortType 62 | } 63 | } 64 | 65 | return listView 66 | } 67 | -------------------------------------------------------------------------------- /client/assets/sass/_bem-support/_mixins.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Block 3 | 4 | example: 5 | +block(block-class) 6 | 7 | creates: 8 | .block-class 9 | 10 | Blocks can be used as parent to Elements or Modifiers 11 | */ 12 | @mixin block($block) { 13 | .#{$block} { 14 | @content; 15 | } 16 | } 17 | 18 | /* 19 | Element 20 | 21 | example: 22 | +block(block-class) 23 | +element(element-class) 24 | +block(block-class2) 25 | +modifier(modifier-class) 26 | +element(element-class) 27 | 28 | creates: 29 | .block-class 30 | .block-class__element-class 31 | .block-class2 32 | .block-class2--modifier 33 | .block-class2--modifier-class .block-class2__element-class 34 | 35 | Elements can be used as children of Blocks or Modifiers on a Block 36 | */ 37 | @mixin element($element) { 38 | $selector: &; 39 | @if containsModifier($selector) { 40 | $block: getBlock($selector); 41 | @at-root { 42 | #{$selector} { 43 | #{$block + $element-separator + $element} { 44 | @content; 45 | } 46 | } 47 | } 48 | } @else { 49 | @at-root { 50 | #{$selector + $element-separator + $element} { 51 | @content; 52 | } 53 | } 54 | } 55 | } 56 | 57 | /* 58 | Modifier 59 | 60 | example: 61 | +block(block-class) 62 | +modifier(modifier-class) 63 | +element(element-class) 64 | +modifier(modifier-class) 65 | 66 | creates: 67 | .block-class 68 | .block-class--element-class 69 | .block-class__element-class 70 | .block-class__element-class--element-class 71 | 72 | Modifiers can be used as children of Blocks or Elements 73 | */ 74 | @mixin modifier($modifier) { 75 | @at-root { 76 | #{&}#{$modifier-separator + $modifier} { 77 | @content; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/app/core/site-switcher/_site-switcher.sass: -------------------------------------------------------------------------------- 1 | /* sass-lint:disable no-color-literals, no-color-keywords, force-element-nesting */ 2 | 3 | .miq-siteswitcher-icon 4 | color: white 5 | 6 | .miq-siteswitcher-link 7 | color: black 8 | 9 | .miq-siteswitcher-entry 10 | border-color: #fff 11 | border-style: solid 12 | border-width: 1px 13 | 14 | display: inline-block 15 | min-width: 90px 16 | padding: 10px 17 | text-align: center 18 | 19 | &:hover 20 | border-color: #bbb 21 | border-style: solid 22 | border-width: 1px 23 | 24 | a 25 | color: #0088ce 26 | 27 | .miq-siteswitcher 28 | .uib-dropdown-menu 29 | left: 8px 30 | margin-top: 11px 31 | min-width: 220px 32 | padding: 9px 33 | 34 | &.uib-dropdown-menu-right 35 | left: auto 36 | right: -2px 37 | 38 | &::after, 39 | &::before 40 | left: auto 41 | right: 6px 42 | 43 | &::after, 44 | &::before 45 | border-bottom-color: #bbb 46 | border-bottom-style: solid 47 | border-bottom-width: 10px 48 | border-left: 10px solid transparent 49 | border-right: 10px solid transparent 50 | content: '' 51 | display: inline-block 52 | left: 6px 53 | position: absolute 54 | top: -11px 55 | 56 | &::after 57 | border-bottom-color: #fff 58 | top: -10px 59 | 60 | &.dropup .uib-dropdown-menu 61 | margin-bottom: 11px 62 | margin-top: 0 63 | 64 | &::after, 65 | &::before 66 | border-bottom: 0 67 | border-top-color: #bbb 68 | border-top-style: solid 69 | border-top-width: 10px 70 | bottom: -11px 71 | top: auto 72 | 73 | &::after 74 | border-top-color: #fff 75 | bottom: -10px 76 | -------------------------------------------------------------------------------- /client/app/services/retire-service-modal/retire-service-modal.html: -------------------------------------------------------------------------------- 1 | 7 | 28 | 38 | -------------------------------------------------------------------------------- /client/app/services/retire-remove-service-modal/retire-remove-service-modal.component.js: -------------------------------------------------------------------------------- 1 | import template from './retire-remove-service-modal.html'; 2 | 3 | export const RetireRemoveServiceModalComponent = { 4 | controller: ComponentController, 5 | controllerAs: 'vm', 6 | bindings: { 7 | resolve: '<', 8 | close: '&', 9 | dismiss: '&' 10 | }, 11 | template, 12 | } 13 | 14 | /** @ngInject */ 15 | function ComponentController ($state, CollectionsApi, EventNotifications) { 16 | const vm = this 17 | 18 | vm.$onInit = function () { 19 | angular.extend(vm, { 20 | services: vm.resolve.services, 21 | isRemove: vm.resolve.modalType === 'remove', 22 | isRetireNow: vm.resolve.modalType === 'retire', 23 | confirm: confirm, 24 | cancel: cancel 25 | }) 26 | } 27 | 28 | function cancel () { 29 | vm.dismiss({$value: 'cancel'}) 30 | } 31 | 32 | function confirm () { 33 | var data = { 34 | action: vm.isRemove ? 'delete' : 'request_retire', 35 | resources: vm.services 36 | } 37 | CollectionsApi.post('services', '', {}, data).then(saveSuccess, saveFailure) 38 | 39 | function saveSuccess (response) { 40 | vm.close() 41 | switch (vm.resolve.modalType) { 42 | case 'retire': 43 | EventNotifications.success(__('Service Retire - Request Created')) 44 | break 45 | case 'remove': 46 | EventNotifications.batch(response.results, __('Service deleting.'), __('Error deleting service.')) 47 | break 48 | } 49 | $state.go($state.current, {}, {reload: true}) 50 | } 51 | 52 | function saveFailure () { 53 | EventNotifications.error(__('There was an error removing one or more services.')) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /client/assets/images/os/os-linux_chrome.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /client/console/common.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebMKS Remote Console 5 | 24 | 25 | 26 |
27 |
28 |
29 | WebMKS 30 | Connecting 31 |
32 |
33 | 44 | 47 |
48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /config/manifest.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const sortBy = require('lodash/sortBy'); 3 | 4 | const uniqueWebpackModules = (data) => { 5 | // recursively flatten nested modules 6 | const transformModule = m => m.modules ? transformModules(m.modules) : m.name.split("!").pop(); 7 | const transformModules = ms => ms.flatMap(m => transformModule(m)); 8 | 9 | var modules = transformModules(data.modules).sort(); 10 | modules = [...new Set(modules)]; // uniq 11 | 12 | return modules; 13 | }; 14 | 15 | const packagesFromModules = (modules) => { 16 | // find the package.json file for each module 17 | modules = modules.filter(m => m.includes("node_modules")); 18 | var packagePaths = modules.map(m => { 19 | var match = m.match(/(.*node_modules\/)([^/]+)(\/[^/]+)?/); 20 | var path = [ 21 | `${match[1]}${match[2]}/package.json`, 22 | `${match[1]}${match[2]}${match[3]}/package.json`, 23 | ].find(p => fs.existsSync(p)); 24 | 25 | if (path == null) { 26 | console.warn(`[webpack-manifest] WARN: Unable to find a package.json for ${m}`); 27 | } 28 | 29 | return path; 30 | }) 31 | packagePaths = packagePaths.filter(p => p != null); 32 | packagePaths = [...new Set(packagePaths)]; // uniq 33 | 34 | // extract relevant package data from the package.json 35 | var packages = packagePaths.map(p => { 36 | var content = fs.readFileSync(p); 37 | var pkg = JSON.parse(content); 38 | return { 39 | name: pkg.name, 40 | license: pkg.license, 41 | version: pkg.version, 42 | location: p 43 | } 44 | }); 45 | packages = sortBy(packages, ['name', 'version']); 46 | 47 | return packages; 48 | }; 49 | 50 | module.exports = { 51 | packagesFromModules, 52 | uniqueWebpackModules, 53 | }; 54 | -------------------------------------------------------------------------------- /client/app/shared/language-switcher/language-switcher.component.js: -------------------------------------------------------------------------------- 1 | import './_language-switcher.sass' 2 | import template from './language-switcher.html'; 3 | 4 | /** @ngInject */ 5 | export const LanguageSwitcherComponent = { 6 | controller: LanguageSwitcherController, 7 | controllerAs: 'vm', 8 | bindings: { 9 | modalInstance: '@?', 10 | mode: '@?' 11 | }, 12 | template, 13 | } 14 | 15 | /** @ngInject */ 16 | function LanguageSwitcherController (Language, lodash, $state, Session) { 17 | const vm = this 18 | 19 | angular.extend(vm, { 20 | switchLanguage: switchLanguage, 21 | available: [] 22 | }) 23 | vm.$onInit = function () { 24 | vm.mode = vm.mode || 'menu' 25 | const hardcoded = { 26 | _browser_: __('Browser Default') 27 | } 28 | 29 | Language.ready 30 | .then(function (available) { 31 | if (vm.mode !== 'menu') { 32 | hardcoded._user_ = __('User Default') 33 | Language.chosen = { code: '_user_' } 34 | } 35 | lodash.forEach(lodash.extend({}, hardcoded, available), (value, key) => { 36 | vm.available.push({ value: key, label: value }) 37 | }) 38 | vm.chosen = lodash.find(vm.available, { 'value': '_user_' }) 39 | }) 40 | } 41 | function switchLanguage (input) { 42 | const languageCode = input.value || input 43 | 44 | if (vm.mode === 'select') { 45 | Language.setLoginLanguage(languageCode) 46 | } else { 47 | Language.setLocale(languageCode) 48 | } 49 | 50 | if (vm.mode === 'menu') { 51 | Language.save(languageCode).then((response) => { 52 | Session.updateUserSession({ settings: { locale: response.data.settings.display.locale } }) 53 | }) 54 | $state.reload() 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /client/app/core/modal/base-modal-controller.spec.js: -------------------------------------------------------------------------------- 1 | /* global inject */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('BaseModalController', () => { 4 | beforeEach(module('app.components')) 5 | 6 | let base, controller, CollectionsApi 7 | const mockModalInstance = {close: angular.noop, dismiss: angular.noop} 8 | 9 | beforeEach(inject(($controller, $injector) => { 10 | CollectionsApi = $injector.get('CollectionsApi') 11 | 12 | base = $controller('BaseModalController', { 13 | $uibModalInstance: mockModalInstance 14 | }) 15 | 16 | controller = angular.extend(angular.copy(base), base) 17 | controller.modalData = { 18 | id: '1', 19 | name: 'bar', 20 | description: 'Your Service' 21 | } 22 | })) 23 | 24 | it('delegates dismiss to the local $uibModalInstance when cancel called', () => { 25 | const spy = sinon.spy(mockModalInstance, 'dismiss') 26 | controller.cancel() 27 | 28 | expect(spy).to.have.been.called 29 | }) 30 | 31 | it('resets the modal data to the original data emitted by the reset', () => { 32 | const payload = {original: {name: 'foo', description: 'My Service'}} 33 | controller.reset(payload) 34 | 35 | expect(controller.modalData.name).to.equal('foo') 36 | expect(controller.modalData.description).to.equal('My Service') 37 | }) 38 | 39 | it('makes a POST request when save is triggered', (done) => { 40 | const spy = sinon.stub(CollectionsApi, 'post').returns(Promise.resolve()) 41 | const payload = {action: 'edit', resource: controller.modalData} 42 | controller.action = 'edit' 43 | controller.collection = 'services' 44 | controller.save() 45 | done() 46 | 47 | expect(spy).to.have.been.calledWith('services', '1', {}, payload) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /client/app/states/orders/details/details.state.js: -------------------------------------------------------------------------------- 1 | import template from './details.html'; 2 | 3 | /** @ngInject */ 4 | export function OrdersDetailsState (routerHelper, RBAC) { 5 | routerHelper.configureStates(getStates(RBAC)) 6 | } 7 | 8 | function getStates (RBAC) { 9 | return { 10 | 'orders.details': { 11 | url: '/:serviceOrderId', 12 | template, 13 | controller: StateController, 14 | controllerAs: 'vm', 15 | title: __('Order Details'), 16 | resolve: { 17 | order: resolveOrder, 18 | serviceTemplate: resolveServiceTemplate 19 | }, 20 | data: { 21 | authorization: RBAC.has('miq_request_show') 22 | } 23 | } 24 | } 25 | } 26 | 27 | /** @ngInject */ 28 | function resolveOrder ($stateParams, CollectionsApi) { 29 | return CollectionsApi.get('service_orders', $stateParams.serviceOrderId, { 30 | expand: ['resources', 'service_requests'] 31 | }) 32 | } 33 | 34 | /** @ngInject */ 35 | function resolveServiceTemplate ($stateParams, CollectionsApi) { 36 | return CollectionsApi.get('service_orders', $stateParams.serviceOrderId, { 37 | expand: ['resources', 'service_requests'] 38 | }).then((ServiceOrder) => { 39 | const serviceTemplateId = ServiceOrder.service_requests[0].source_id; 40 | return CollectionsApi.get('service_templates', serviceTemplateId, { 41 | expand: ['resources'], 42 | attributes: ['picture', 'resource_actions', 'picture.image_href'], 43 | }) 44 | }) 45 | } 46 | 47 | /** @ngInject */ 48 | function StateController (order, serviceTemplate, $state) { 49 | const vm = this 50 | vm.order = order 51 | vm.serviceTemplate = serviceTemplate 52 | 53 | vm.requestListConfig = { 54 | showSelectBox: false, 55 | selectionMatchProp: 'id' 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /client/app/shared/custom-dropdown/custom-dropdown.html: -------------------------------------------------------------------------------- 1 | 2 | 13 | 35 | 36 | -------------------------------------------------------------------------------- /client/app/states/orders/details/details.html: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 21 | 23 | 25 |
26 |
27 |

{{'Order #' |translate}}{{vm.order.id}}

28 |

{{'ordered on' | translate}} {{ ::(vm.order.placed_at || vm.order.updated_at) | date }}

29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 | -------------------------------------------------------------------------------- /client/assets/images/providers/vendor-jboss-eap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 26 | 27 | 28 | 29 | 30 | 33 | 40 | 47 | EAP 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: true 4 | 5 | nodeLinker: node-modules 6 | 7 | npmAuditExcludePackages: 8 | - angular 9 | # pending | high | GHSA-4w4v-5hc9-xrr2 | angular >=1.3.0 <=1.8.3 | 1.8.3 brought in by manageiq-ui-service@workspace:. 10 | # pending | moderate | GHSA-2qqx-w9hr-q5gx | angular <=1.8.3 | 1.8.3 brought in by manageiq-ui-service@workspace:. 11 | # pending | moderate | GHSA-2vrf-hf26-jrp5 | angular <=1.8.3 | 1.8.3 brought in by manageiq-ui-service@workspace:. 12 | # pending | moderate | GHSA-m2h2-264f-f486 | angular >=1.7.0 | 1.8.3 brought in by manageiq-ui-service@workspace:. 13 | # pending | moderate | GHSA-prc3-vjfx-vhm9 | angular <=1.8.3 | 1.8.3 brought in by manageiq-ui-service@workspace:. 14 | # pending | moderate | GHSA-qwqh-hm9m-p5hr | angular <=1.8.3 | 1.8.3 brought in by manageiq-ui-service@workspace:. 15 | # pending | low | GHSA-j58c-ww9w-pwp5 | angular <=1.8.3 | 1.8.3 brought in by manageiq-ui-service@workspace:. 16 | # pending | low | GHSA-m9gf-397r-hwpg | angular >=1.3.0-rc.4 <=1.8.3 | 1.8.3 brought in by manageiq-ui-service@workspace:. 17 | # pending | low | GHSA-mqm9-c95h-x2p6 | angular <=1.8.3 | 1.8.3 brought in by manageiq-ui-service@workspace:. 18 | - angular-sanitize 19 | # pending | moderate | GHSA-4p4w-6hg8-63wx | angular-sanitize >=1.3.1 <=1.8.3 | 1.8.3 brought in by manageiq-ui-service@workspace:. 20 | - bootstrap 21 | # pending | moderate | GHSA-9mvj-f7w8-pvh2 | bootstrap >=2.0.0 <=3.4.1 | 3.4.1 brought in by angular-patternfly@npm:5.0.3 22 | - bootstrap-sass 23 | # pending | moderate | GHSA-9mvj-f7w8-pvh2 | bootstrap-sass >=2.0.0 <=3.4.3 | 3.4.3 brought in by patternfly@npm:3.59.5 24 | 25 | yarnPath: .yarn/releases/yarn-4.12.0.cjs 26 | -------------------------------------------------------------------------------- /client/app/core/dialog-field-refresh.service.js: -------------------------------------------------------------------------------- 1 | /* eslint camelcase: "off" */ 2 | /* eslint angular/angularelement: "off" */ 3 | 4 | /** @ngInject */ 5 | export function DialogFieldRefreshFactory (CollectionsApi, DialogData) { 6 | var service = { 7 | refreshDialogField: refreshDialogField, 8 | setFieldValueDefaults: setFieldValueDefaults 9 | } 10 | 11 | return service 12 | 13 | function refreshDialogField (dialogData, dialogField, url, idList) { 14 | let data = { 15 | action: 'refresh_dialog_fields', 16 | resource: { 17 | dialog_fields: DialogData.outputConversion(dialogData), 18 | fields: dialogField, 19 | resource_action_id: idList.resourceActionId, 20 | target_id: idList.targetId, 21 | target_type: idList.targetType, 22 | }, 23 | }; 24 | 25 | return CollectionsApi.post(url, idList.dialogId, {}, angular.toJson(data)) 26 | .then((response) => response.result[dialogField]); 27 | } 28 | 29 | function setFieldValueDefaults (dialog, defaultValues) { 30 | const fieldValues = {} 31 | for (var option in defaultValues) { 32 | fieldValues[option.replace('dialog_', '')] = defaultValues[option] 33 | } 34 | // Just for user reference for dialog heirarchy dialog => tabs => groups => fields => field 35 | dialog.dialog_tabs.forEach((tab, tab_index) => { // tabs 36 | tab.dialog_groups.forEach((group, group_index) => { // groups 37 | group.dialog_fields.forEach((field, field_index) => { // fields 38 | const fieldValue = (angular.isDefined(fieldValues[field.name]) ? fieldValues[field.name] : field.default_value) 39 | dialog.dialog_tabs[tab_index].dialog_groups[group_index].dialog_fields[field_index].default_value = fieldValue 40 | }) 41 | }) 42 | }) 43 | 44 | return dialog 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/app/shared/action-button-group/action-button-group.html: -------------------------------------------------------------------------------- 1 | 6 | 13 | 20 | 26 | 33 | 38 | 45 | 50 | -------------------------------------------------------------------------------- /client/app/core/tag-editor-modal/tag-editor-modal.service.js: -------------------------------------------------------------------------------- 1 | import template from './tag-editor-modal.html'; 2 | 3 | /** @ngInject */ 4 | export function TagEditorFactory ($uibModal) { 5 | var modalService = { 6 | showModal: showModal 7 | } 8 | 9 | return modalService 10 | 11 | function showModal (services, tags) { 12 | var modalOptions = { 13 | template, 14 | controller: TagEditorModalController, 15 | controllerAs: 'vm', 16 | size: 'md', 17 | resolve: { 18 | services: resolveServices, 19 | tags: resolveTags 20 | } 21 | } 22 | var modal = $uibModal.open(modalOptions) 23 | 24 | return modal.result 25 | 26 | function resolveServices () { 27 | return services 28 | } 29 | 30 | function resolveTags () { 31 | return tags 32 | } 33 | } 34 | } 35 | 36 | /** @ngInject */ 37 | function TagEditorModalController (services, tags, $controller, $uibModalInstance, 38 | $state, TaggingService, EventNotifications) { 39 | const vm = this 40 | var base = $controller('BaseModalController', { 41 | $uibModalInstance: $uibModalInstance 42 | }) 43 | angular.extend(vm, base) 44 | 45 | vm.save = save 46 | vm.services = angular.isArray(services) ? services : [services] 47 | vm.modalData = { tags: angular.copy(tags) } 48 | 49 | // Override 50 | function save () { 51 | return TaggingService.assignTags('services', vm.services, tags, vm.modalData.tags) 52 | .then(saveSuccess) 53 | .catch(saveFailure) 54 | 55 | function saveSuccess () { 56 | $uibModalInstance.close() 57 | EventNotifications.success(__('Tagging successful.')) 58 | $state.go($state.current, {}, {reload: true}) 59 | } 60 | 61 | function saveFailure () { 62 | EventNotifications.error(__('There was an error tagging this service.')) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /client/assets/sass/_overrides.sass: -------------------------------------------------------------------------------- 1 | // sass-lint:disable no-important 2 | 3 | // Over-ridden until fixed in angular patternfly 4 | .pf-expand-placeholder 5 | margin-right: 15px 6 | 7 | // Over-ridden for less margins 8 | .list-group-item-header 9 | .list-view-pf-actions 10 | margin: 0 11 | 12 | button 13 | margin-bottom: 4px 14 | 15 | // This should be in patternfly 16 | .dropdown-menu-appended-to-body 17 | &::before, 18 | &::after 19 | border-bottom-color: $color-gray-border 20 | border-bottom-style: solid 21 | border-bottom-width: 10px 22 | border-left: 10px solid transparent 23 | border-right: 10px solid transparent 24 | content: '' 25 | display: inline-block 26 | left: 6px 27 | position: absolute 28 | top: -11px 29 | 30 | &::after 31 | border-bottom-color: $color-white 32 | top: -10px 33 | 34 | &.dropdown-menu-right 35 | &::before, 36 | &::after 37 | left: auto 38 | right: 6px 39 | 40 | // Override the ui-commponents additional margin on toolbars 41 | .toolbar-pf 42 | .toolbar-pf-actions 43 | 44 | .form-group 45 | margin-bottom: 0 !important 46 | 47 | // Override margin-left added to parent of pfFilter complex select 48 | .btn-group + .dropdown 49 | margin-left: 0 50 | 51 | // pf-toolbar-dropdown goes crazy when given sufficient cause to, this keeps it inline 52 | .toolbar-apf-filter 53 | .category-select 54 | .dropdown-toggle 55 | width: 147px 56 | 57 | .dropdown-menu 58 | max-height: 78em 59 | max-width: 202px 60 | overflow-y: scroll 61 | 62 | li 63 | overflow-x: scroll 64 | 65 | // Override patternfly-timeline default icon size 66 | .timeline-pf-drop 67 | font-size: 20px !important 68 | 69 | &:hover 70 | font-size: 30px !important 71 | 72 | .blank-slate-pf 73 | padding: 30px !important 74 | -------------------------------------------------------------------------------- /client/app/shared/language-switcher/language-switcher.component.spec.js: -------------------------------------------------------------------------------- 1 | /* global $componentController, Language */ 2 | /* eslint-disable no-unused-expressions */ 3 | describe('Component: languageSwitcher ', function () { 4 | let ctrl 5 | let languageSpy 6 | 7 | describe('Language switch select list', () => { 8 | beforeEach(function () { 9 | module('app.core', 'app.shared') 10 | bard.inject('$componentController', 'Language') 11 | 12 | ctrl = $componentController('languageSwitcher', {}, { 13 | 'mode': 'select' 14 | }) 15 | languageSpy = sinon.stub(Language, 'ready').returns((Promise.resolve([]))) 16 | }) 17 | 18 | it('is defined', function () { 19 | expect(ctrl).to.exist; 20 | }) 21 | 22 | it('allows for the component to initialize', (done) => { 23 | ctrl.$onInit() 24 | done() 25 | expect(languageSpy).to.have.been.called 26 | }) 27 | 28 | it('allows for a language to be set via login select menu', (done) => { 29 | languageSpy = sinon.stub(Language, 'setLoginLanguage').returns(true) 30 | ctrl.switchLanguage('fr') 31 | done() 32 | expect(languageSpy).to.have.been.calledWith('fr') 33 | }) 34 | }) 35 | describe('Language switch Menu', () => { 36 | beforeEach(function () { 37 | module('app.core', 'app.shared') 38 | bard.inject('$componentController', 'Language') 39 | 40 | ctrl = $componentController('languageSwitcher', {}, { 41 | 'mode': 'menu' 42 | }) 43 | languageSpy = sinon.stub(Language, 'ready').returns((Promise.resolve([]))) 44 | }) 45 | 46 | it('allows for a language to be changed via menu', (done) => { 47 | languageSpy = sinon.stub(Language, 'save').returns((Promise.resolve('success'))) 48 | ctrl.switchLanguage('fr') 49 | done() 50 | expect(languageSpy).to.have.been.calledWith('fa') 51 | }) 52 | }) 53 | }) 54 | --------------------------------------------------------------------------------