',
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 |
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 |
3 |
4 |
5 |
13 |
14 |
--------------------------------------------------------------------------------
/client/app/orders/process-order-modal/process-order-modal.html:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 | {{ vm.order.name }} and all of its service requests will be permanently removed!
10 |
11 | Affected Order
12 |
13 |
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 |
6 |
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 |
9 |
15 |
16 |
17 |
18 |
Affected Services
19 |
20 |
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: ''
14 | },
15 | transclude: true,
16 | template,
17 | }
18 |
19 | /** @ngInject */
20 | function ComponentController ($transclude) {
21 | const vm = this
22 | vm.$onInit = () => {
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 |
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 |
2 |
3 |
4 | {{'Service Catalog'|translate}}
5 |
6 |
7 |
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 |
9 |
10 | {{ "Warning: The selected Services and ALL of their components will be permanently removed!" | translate }}
11 | {{ "Warning: The selected Services and ALL of their components will be immediately retired!" | translate }}
12 |
13 | Affected Services
14 |
15 |
16 |
23 |
24 |
--------------------------------------------------------------------------------
/client/app/core/save-modal-dialog/save-modal-dialog.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
If you do not save, any changes will be lost.
11 | Your current edits are invalid and cannot be saved. Continuing will discard your changes.
12 |
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 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
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 |
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: ''
9 | },
10 | template,
11 | }
12 |
13 | /** @ngInject */
14 | function ComponentController ($state, ShoppingCart, EventNotifications) {
15 | const vm = this
16 |
17 | vm.$doCheck = refresh
18 |
19 | vm.submit = submit
20 | vm.close = close
21 | vm.clear = ShoppingCart.reset
22 | vm.remove = ShoppingCart.removeItem
23 | vm.state = null
24 |
25 | /**
26 | * Refreshes shopping cart state
27 | * @function refresh
28 | */
29 | function refresh () {
30 | vm.state = ShoppingCart.state()
31 | }
32 |
33 | /**
34 | * Submits a shopping cart
35 | * @function submit
36 | * @returns Promise
37 | */
38 | function submit () {
39 | return ShoppingCart.submit()
40 | .then(function () {
41 | EventNotifications.success(__('Shopping cart successfully ordered'))
42 | vm.modalInstance.close()
43 | $state.go('orders')
44 | })
45 | .then(null, function (err) {
46 | EventNotifications.error(__('There was an error submitting this request: ') + err)
47 | })
48 | }
49 |
50 | /**
51 | * closes a shopping cart modal
52 | * @function close
53 | */
54 | function close () {
55 | vm.modalInstance.close()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/client/assets/images/providers/vendor-amazon.svg:
--------------------------------------------------------------------------------
1 |
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 |
9 |
{{ ::vm.message }}
10 |
23 |
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 |
2 |
3 | {{'Service Catalogs'|translate}}
4 |
5 |
6 |
7 | {{'Service'|translate}}: {{ ::vm.service.name }}
8 |
9 |
10 | Custom Button: {{ ::vm.button.name }}
11 |
12 |
13 |
26 |
27 |
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 |
8 |
9 |
10 |
12 |
13 |
14 |
15 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
Affected Services
26 |
27 |
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 |
34 | English
35 | Japanese
36 | German
37 | Italian
38 | Spanish
39 | Portuguese
40 | French
41 | Swiss‐French
42 | Swiss‐German
43 |
44 |
45 |
46 |
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 |
9 |
10 | {{vm.config.title}}
11 |
12 |
13 |
35 |
36 |
--------------------------------------------------------------------------------
/client/app/states/orders/details/details.html:
--------------------------------------------------------------------------------
1 |
11 |
12 |
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 | Cancel
5 |
6 | Reset
12 |
13 |
18 | {{vm.onSaveLabel}}
19 |
20 | OK
25 |
26 | OK
32 |
33 | {{ vm.customButtonTranslated }}
37 |
38 | Reset
44 |
45 | Cancel
49 |
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 |
--------------------------------------------------------------------------------