diff --git a/pkg/kubernetes/scripts/virtual-machines.js b/pkg/kubernetes/scripts/virtual-machines.js index 1b5319a13b0a..2199beaf9ff1 100644 --- a/pkg/kubernetes/scripts/virtual-machines.js +++ b/pkg/kubernetes/scripts/virtual-machines.js @@ -25,9 +25,11 @@ require('angular-dialog.js'); require('./kube-client'); require('./listing'); - var vmsReact = require('./virtual-machines/index.jsx'); + var vmsReact = require('./virtual-machines/entry-points/virtual-machines.jsx'); + var vmReact = require('./virtual-machines/entry-points/virtual-machine.jsx'); require('../views/virtual-machines-page.html'); + require('../views/virtual-machine-page.html'); angular.module('kubernetes.virtualMachines', [ 'ngRoute', @@ -45,6 +47,13 @@ .when('/vms', { templateUrl: 'views/virtual-machines-page.html', controller: 'VirtualMachinesCtrl' + }) + .when('/vms/:namespace/:name', { + templateUrl: 'views/virtual-machine-page.html', + controller: 'VirtualMachineCtrl' + }) + .when('/vms/:namespace', { + redirectTo: '/vms' }); /* Links rewriting is enabled by default. It does two things: @@ -76,6 +85,18 @@ function($scope, loader, select, methods, request) { vmsReact.init($scope, loader, select, methods, request); }] + ) + + .controller('VirtualMachineCtrl', [ + '$scope', + '$routeParams', + 'kubeLoader', + 'kubeSelect', + 'kubeMethods', + 'KubeRequest', + function($scope, $routeParams, loader, select, methods, request) { + vmReact.init($scope, $routeParams, loader, select, methods, request); + }] ); }()); diff --git a/pkg/kubernetes/scripts/virtual-machines/components/VmActions.jsx b/pkg/kubernetes/scripts/virtual-machines/components/VmActions.jsx index f93b7a6ad6de..ce748ebf7fed 100644 --- a/pkg/kubernetes/scripts/virtual-machines/components/VmActions.jsx +++ b/pkg/kubernetes/scripts/virtual-machines/components/VmActions.jsx @@ -30,17 +30,18 @@ import { vmActionFailed } from '../action-creators.jsx'; // import DropdownButtons from '../../../../machines/components/dropdownButtons.jsx'; -const VmActions = ({ vm, onFailure }: { vm: Vm, onFailure: Function }) => { +const VmActions = ({ vm, onDeleteSuccess, onDeleteFailure }: { vm: Vm, onDeleteSuccess: Function, onDeleteFailure: Function }) => { const id = vmIdPrefx(vm); const onDelete = () => { - vmDelete({ vm }).catch((error) => { - console.info('VmActions: delete failed: ', error); - onFailure({ - message: _("VM DELETE failed."), - detail: error, - }); - }); + vmDelete({ vm }).then(onDeleteSuccess) + .catch((error) => { + console.info('VmActions: delete failed: ', error); + onDeleteFailure({ + message: _("VM DELETE failed."), + detail: error, + }); + }); }; const buttonDelete = ( @@ -56,12 +57,13 @@ const VmActions = ({ vm, onFailure }: { vm: Vm, onFailure: Function }) => { VmActions.propTypes = { vm: PropTypes.object.isRequired, - onFailure: PropTypes.func.isRequired, + onDeleteSuccess: PropTypes.func.isRequired, + onDeleteFailure: PropTypes.func.isRequired, }; export default connect( () => ({}), (dispatch, { vm }) => ({ - onFailure: ({ message, detail }) => dispatch(vmActionFailed({ vm, message, detail })), + onDeleteFailure: ({ message, detail }) => dispatch(vmActionFailed({ vm, message, detail })), }), )(VmActions); diff --git a/pkg/kubernetes/scripts/virtual-machines/components/VmDetail.jsx b/pkg/kubernetes/scripts/virtual-machines/components/VmDetail.jsx new file mode 100644 index 000000000000..573d286ba687 --- /dev/null +++ b/pkg/kubernetes/scripts/virtual-machines/components/VmDetail.jsx @@ -0,0 +1,104 @@ +/* + * This file is part of Cockpit. + * + * Copyright (C) 2018 Red Hat, Inc. + * + * Cockpit is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * Cockpit is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Cockpit; If not, see . + */ + +// @flow + +import React, { PropTypes } from 'react'; +import cockpit, { gettext as _ } from 'cockpit'; +import { connect } from "react-redux"; + +import { DetailPage, DetailPageRow, DetailPageHeader } from 'cockpit-components-detail-page.jsx'; +import { getPod, getPodMetrics } from '../selectors.jsx'; +import VmOverviewTab from './VmOverviewTabKubevirt.jsx'; +import VmActions from './VmActions.jsx'; +import VmMetricsTab from './VmMetricsTab.jsx'; +import VmDisksTab from './VmDisksTabKubevirt.jsx'; + +import type { Vm, VmMessages, PersistenVolumes, Pod } from '../types.jsx'; +import { vmIdPrefx, prefixedId } from '../utils.jsx'; + +const navigateToVms = () => { + cockpit.location.go([ 'vms' ]); +}; + +const VmDetail = ({ vm, pageParams, vmMessages, pvs, pod, podMetrics }: + { vm: Vm, vmMessages: VmMessages, pageParams: Object, pvs: PersistenVolumes, pod: Pod}) => { + const mainTitle = vm ? `${vm.metadata.namespace}:${vm.metadata.name}` : null; + const actions = vm ? : null; + const header = (); + + if (!vm) { + return ( +
+ + {header} + + +
+ ); + } + const idPrefix = vmIdPrefx(vm); + + return ( +
+ + {header} + + + + + + + + + + +
+ ); +}; + +VmDetail.propTypes = { + vm: PropTypes.object.isRequired, + pageParams: PropTypes.object, + vmMessages: PropTypes.object, + pvs: PropTypes.array.isRequired, + pod: PropTypes.object.isRequired, + podMetrics: PropTypes.object, +}; + +export default connect( + ({vms, pvs, pods, vmsMessages, nodeMetrics}) => { + const vm = vms.length > 0 ? vms[0] : null; + const pod = getPod(vm, pods); + const podMetrics = getPodMetrics(pod, nodeMetrics); + return { + vm, + vmMessages: vm ? vmsMessages[vm.metadata.uid] : null, + pvs, + pod, + podMetrics, + }; + }, +)(VmDetail); diff --git a/pkg/kubernetes/scripts/virtual-machines/components/VmDisksTabKubevirt.jsx b/pkg/kubernetes/scripts/virtual-machines/components/VmDisksTabKubevirt.jsx index 58312d22dbe2..95ff7e7b9c56 100644 --- a/pkg/kubernetes/scripts/virtual-machines/components/VmDisksTabKubevirt.jsx +++ b/pkg/kubernetes/scripts/virtual-machines/components/VmDisksTabKubevirt.jsx @@ -66,7 +66,7 @@ const prepareDiskData = (disk, vm, pvs, idPrefix) => { if (pv) { bus = _("iSCSI"); onNavigate = () => cockpit.jump(`/kubernetes#/volumes/${pv.metadata.name}`); - } else if (disk.disk.bus) { + } else if (disk.disk && disk.disk.bus) { bus = disk.disk.bus; } diff --git a/pkg/kubernetes/scripts/virtual-machines/components/VmMetricsTab.jsx b/pkg/kubernetes/scripts/virtual-machines/components/VmMetricsTab.jsx index 10e06f61a756..3e7207d26755 100644 --- a/pkg/kubernetes/scripts/virtual-machines/components/VmMetricsTab.jsx +++ b/pkg/kubernetes/scripts/virtual-machines/components/VmMetricsTab.jsx @@ -21,6 +21,7 @@ import React, { PropTypes } from 'react'; import { DonutChart } from 'patternfly-react'; import cockpit, { gettext as _ } from 'cockpit'; +import { prefixedId } from '../utils.jsx'; import './VmMetricsTab.less'; @@ -38,11 +39,11 @@ const MetricColumn = ({ type, children, className }) => { const MetricColumnContent = ({ id, title, smallTitle }) => { return (
- + {title}
- + {smallTitle}
@@ -79,33 +80,33 @@ const CPUChart = ({ id, podMetrics }) => { ); }; -const CpuColumn = ({ podMetrics }) => { +const CpuColumn = ({ idPrefix, podMetrics }) => { return ( - + ); }; -const MemoryColumn = ({ podMetrics }) => { +const MemoryColumn = ({ idPrefix, podMetrics }) => { const usage = cockpit.format_bytes(podMetrics.memory.usageBytes); return ( - + ); }; -const NetworkColumn = ({ podMetrics }) => { +const NetworkColumn = ({ idPrefix, podMetrics }) => { const received = cockpit.format_bytes_per_sec(podMetrics.network.rxBytes); const transmitted = cockpit.format_bytes_per_sec(podMetrics.network.txBytes); const title = (
-
+
{received}
-
+
{transmitted}
@@ -113,27 +114,27 @@ const NetworkColumn = ({ podMetrics }) => { ); return ( - + ); }; -const PodMetrics = ({ podMetrics }) => { +const PodMetrics = ({ idPrefix, podMetrics }) => { return (
- - - + + +
); }; -const VmMetricsTab = ({podMetrics}) => { - const content = podMetrics ? () +const VmMetricsTab = ({idPrefix, podMetrics}) => { + const content = podMetrics ? () : _("Usage metrics are available after the pod starts"); return ( -
+
{content}
); diff --git a/pkg/kubernetes/scripts/virtual-machines/components/VmOverviewTabKubevirt.jsx b/pkg/kubernetes/scripts/virtual-machines/components/VmOverviewTabKubevirt.jsx index ce5c0354bf44..af4e5a21b8a6 100644 --- a/pkg/kubernetes/scripts/virtual-machines/components/VmOverviewTabKubevirt.jsx +++ b/pkg/kubernetes/scripts/virtual-machines/components/VmOverviewTabKubevirt.jsx @@ -64,7 +64,7 @@ const PodLink = ({ pod }) => { return ({pod.metadata.name}); }; -const VmOverviewTabKubevirt = ({ vm, vmMessages, pod }: { vm: Vm, vmMessages: VmMessages, pod: Pod }) => { +const VmOverviewTabKubevirt = ({ vm, vmMessages, pod, showState }: { vm: Vm, vmMessages: VmMessages, pod: Pod, showState: boolean }) => { const idPrefix = vmIdPrefx(vm); const message = (); @@ -73,12 +73,25 @@ const VmOverviewTabKubevirt = ({ vm, vmMessages, pod }: { vm: Vm, vmMessages: Vm const nodeLink = nodeName ? ({nodeName}) : '-'; const podLink = (); - const items = [ - {title: commonTitles.MEMORY, value: getMemory(vm), idPostfix: 'memory'}, - {title: _("Node:"), value: nodeLink, idPostfix: 'node'}, - {title: commonTitles.CPUS, value: _(getValueOrDefault(() => vm.spec.domain.cpu.cores, 1)), idPostfix: 'vcpus'}, - {title: _("Labels:"), value: getLabels(vm), idPostfix: 'labels'}, - {title: _("Pod:"), value: podLink, idPostfix: 'pod'}, + const memoryItem = {title: commonTitles.MEMORY, value: getMemory(vm), idPostfix: 'memory'}; + const vCpusItem = {title: commonTitles.CPUS, value: _(getValueOrDefault(() => vm.spec.domain.cpu.cores, 1)), idPostfix: 'vcpus'}; + const podItem = {title: _("Pod:"), value: podLink, idPostfix: 'pod'}; + const nodeItem = {title: _("Node:"), value: nodeLink, idPostfix: 'node'}; + const labelsItem = {title: _("Labels:"), value: getLabels(vm), idPostfix: 'labels'}; + + const items = showState ? [ + memoryItem, + {title: _("State"), value: getValueOrDefault(() => vm.status.phase, _("n/a")), idPostfix: 'state'}, + vCpusItem, + nodeItem, + podItem, + labelsItem, + ] : [ + memoryItem, + nodeItem, + vCpusItem, + labelsItem, + podItem, ]; return ( { + return cockpit.location.go([ 'vms', vm.metadata.namespace, vm.metadata.name ]); +}; + const VmsListingRow = ({ vm, vmMessages, pvs, pod, podMetrics, vmUi, onExpandChanged }: { vm: Vm, vmMessages: VmMessages, pvs: PersistenVolumes, pod: Pod, onExpandChanged: Function }) => { - const node = (vm.metadata.labels && vm.metadata.labels[NODE_LABEL]) || '-'; - const phase = (vm.status && vm.status.phase) || _("n/a"); + const node = getValueOrDefault(() => vm.metadata.labels[NODE_LABEL], '-'); + const phase = getValueOrDefault(() => vm.status.phase, _("n/a")); const idPrefix = vmIdPrefx(vm); const overviewTabRenderer = { - name: (
{_("Overview")}
), + name: (
{_("Overview")}
), renderer: VmOverviewTab, data: { vm, vmMessages, pod }, presence: 'always', }; const metricsTabRenderer = { - name: (
{_("Usage")}
), + name: (
{_("Usage")}
), renderer: VmMetricsTab, - data: { podMetrics }, + data: { idPrefix, podMetrics }, presence: 'always', }; const disksTabRenderer = { - name: (
{_("Disks")}
), + name: (
{_("Disks")}
), renderer: VmDisksTab, data: { vm, pvs }, presence: 'onlyActive', @@ -74,6 +78,7 @@ const VmsListingRow = ({ vm, vmMessages, pvs, pod, podMetrics, vmUi, onExpandCha tabRenderers={[ overviewTabRenderer, metricsTabRenderer, disksTabRenderer ]} listingActions={[ ]} expandChanged={onExpandChanged(vm)} + navigateToItem={navigateToVm.bind(this, vm)} initiallyExpanded={initiallyExpanded} /> ); }; @@ -83,6 +88,7 @@ VmsListingRow.propTypes = { vmMessages: PropTypes.object, pvs: PropTypes.array.isRequired, pod: PropTypes.object.isRequired, + podMetrics: PropTypes.object, vmUi: PropTypes.object, onExpandChanged: PropTypes.func.isRequired, }; diff --git a/pkg/kubernetes/scripts/virtual-machines/entry-points/util/initialize.es6 b/pkg/kubernetes/scripts/virtual-machines/entry-points/util/initialize.es6 new file mode 100644 index 000000000000..30350769a08f --- /dev/null +++ b/pkg/kubernetes/scripts/virtual-machines/entry-points/util/initialize.es6 @@ -0,0 +1,65 @@ +/* + * This file is part of Cockpit. + * + * Copyright (C) 2018 Red Hat, Inc. + * + * Cockpit is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * Cockpit is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Cockpit; If not, see . + */ + +import { initMiddleware } from '../../kube-middleware.jsx'; +import { watchMetrics, cleanupMetricsWatch } from '../../watch-metrics.es6'; +import * as actionCreators from '../../action-creators.jsx'; + +function addKubeLoaderListener (store, $scope, kubeLoader, kubeSelect) { + // register load callback( callback, until ) + kubeLoader.listen(() => { + const persistentVolumes = kubeSelect().kind('PersistentVolume'); + const pods = kubeSelect().kind('Pod'); + + store.dispatch(actionCreators.setPVs(Object.values(persistentVolumes))); + store.dispatch(actionCreators.setPods(Object.values(pods))); + }, $scope); + + // enable watching( watched-entity-type, until ) + kubeLoader.watch('PersistentVolume', $scope); + kubeLoader.watch('Pod', $scope); +} + +function addScopeVarsToStore (store, $scope) { + $scope.$watch( + scope => scope.settings, + newSettings => store.dispatch(actionCreators.setSettings(newSettings))); +} + +/** + * + * @param {$rootScope.Scope} $scope '.*Ctrl' controller scope + * @param {kubeLoader} kubeLoader + * @param {kubeSelect} kubeSelect + * @param {kubeMethods} kubeMethods + * @param {KubeRequest} KubeRequest + * @param {store} store + * @param {onDestroy} onDestroy + */ +export default function initialize($scope, kubeLoader, kubeSelect, kubeMethods, KubeRequest, store, onDestroy) { + addKubeLoaderListener(store, $scope, kubeLoader, kubeSelect); + initMiddleware(kubeMethods, kubeLoader, KubeRequest); + addScopeVarsToStore(store, $scope); + + watchMetrics(store); + $scope.$on("$destroy", () => { + typeof onDestroy === 'function' && onDestroy(); + cleanupMetricsWatch(); + }); +} diff --git a/pkg/kubernetes/scripts/virtual-machines/entry-points/virtual-machine.jsx b/pkg/kubernetes/scripts/virtual-machines/entry-points/virtual-machine.jsx new file mode 100644 index 000000000000..e526f8a34b56 --- /dev/null +++ b/pkg/kubernetes/scripts/virtual-machines/entry-points/virtual-machine.jsx @@ -0,0 +1,96 @@ +/* + * This file is part of Cockpit. + * + * Copyright (C) 2018 Red Hat, Inc. + * + * Cockpit is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * Cockpit is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Cockpit; If not, see . + */ + +import 'regenerator-runtime/runtime'; // required for library initialization +import React from 'react'; +import { Provider } from 'react-redux'; + +import { setVms, vmExpanded } from '../action-creators.jsx'; +import VmDetail from '../components/VmDetail.jsx'; +import { initStore, getStore } from '../store.es6'; +import initialize from './util/initialize.es6'; + +import '../../../../machines/machines.less'; // once per component hierarchy + +const VmPage = ({pageParams}) => ( + + + +); + +function addVmListener (store, $scope, kubeLoader, kubeSelect, namespace, name) { + kubeLoader.listen(() => { + const vm = kubeSelect().kind('VirtualMachine') + .namespace(namespace) + .name(name) + .one(); + const result = vm ? [vm] : []; + + if (vm) { + store.dispatch(vmExpanded({ + vm, + isExpanded: true + })); + } + + store.dispatch(setVms(result)); + }, $scope); + kubeLoader.watch('VirtualMachine', $scope); +} + +/** + * + * @param {$rootScope.Scope} $scope 'VirtualMachinesCtrl' controller scope + * @param {$routeParams} $routeParams + * @param {kubeLoader} kubeLoader + * @param {kubeSelect} kubeSelect + * @param {kubeMethods} kubeMethods + * @param {KubeRequest} KubeRequest + */ +function init ($scope, $routeParams, kubeLoader, kubeSelect, kubeMethods, KubeRequest) { + const store = initStore(); + const name = $routeParams.name; + const namespace = $routeParams.namespace; + + let onDestroy; + if (namespace && name) { + // enable metrics fetching + onDestroy = () => { + const state = store.getState(); + if (state.vms.length > 0) { + store.dispatch(vmExpanded({ + vm: state.vms[0], + isExpanded: false, + })); + } + }; + + // fetch only if there is a namespace and name + addVmListener(store, $scope, kubeLoader, kubeSelect, namespace, name); + } else { + // otherwise reset + store.dispatch(setVms([])); + } + initialize($scope, kubeLoader, kubeSelect, kubeMethods, KubeRequest, store, onDestroy); + + const rootElement = document.querySelector('#kubernetes-virtual-machine-root'); + React.render(, rootElement); +} + +export { init }; diff --git a/pkg/kubernetes/scripts/virtual-machines/entry-points/virtual-machines.jsx b/pkg/kubernetes/scripts/virtual-machines/entry-points/virtual-machines.jsx new file mode 100644 index 000000000000..7c563341ac5b --- /dev/null +++ b/pkg/kubernetes/scripts/virtual-machines/entry-points/virtual-machines.jsx @@ -0,0 +1,62 @@ +/* + * This file is part of Cockpit. + * + * Copyright (C) 2018 Red Hat, Inc. + * + * Cockpit is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * Cockpit is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Cockpit; If not, see . + */ + +import 'regenerator-runtime/runtime'; // required for library initialization +import React from 'react'; +import { Provider } from 'react-redux'; + +import VmsListing from '../components/VmsListing.jsx'; +import { initStore, getStore } from '../store.es6'; +import initialize from './util/initialize.es6'; +import { setVms } from '../action-creators.jsx'; + +import '../../../../machines/machines.less'; // once per component hierarchy + +const VmsPage = () => ( + + + +); + +function addVmsListener (store, $scope, kubeLoader, kubeSelect) { + kubeLoader.listen(() => { + const vms = kubeSelect().kind('VirtualMachine'); + store.dispatch(setVms(Object.values(vms))); + }, $scope); + kubeLoader.watch('VirtualMachine', $scope); +} + +/** + * + * @param {$rootScope.Scope} $scope 'VirtualMachinesCtrl' controller scope + * @param {kubeLoader} kubeLoader + * @param {kubeSelect} kubeSelect + * @param {kubeMethods} kubeMethods + * @param {KubeRequest} KubeRequest + */ +function init ($scope, kubeLoader, kubeSelect, kubeMethods, KubeRequest) { + const store = initStore(); + addVmsListener(store, $scope, kubeLoader, kubeSelect); + initialize($scope, kubeLoader, kubeSelect, kubeMethods, KubeRequest, store); + + const rootElement = document.querySelector('#kubernetes-virtual-machines-root'); + React.render(, rootElement); +} + +export { init }; diff --git a/pkg/kubernetes/scripts/virtual-machines/index.jsx b/pkg/kubernetes/scripts/virtual-machines/index.jsx deleted file mode 100644 index 0637f4b6ca96..000000000000 --- a/pkg/kubernetes/scripts/virtual-machines/index.jsx +++ /dev/null @@ -1,100 +0,0 @@ -/* - * This file is part of Cockpit. - * - * Copyright (C) 2017 Red Hat, Inc. - * - * Cockpit is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation; either version 2.1 of the License, or - * (at your option) any later version. - * - * Cockpit is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Cockpit; If not, see . - */ - -import 'regenerator-runtime/runtime'; // required for library initialization -import React from 'react'; -import { createStore } from 'redux'; -import { Provider } from 'react-redux'; - -import { initMiddleware } from './kube-middleware.jsx'; -import reducers from './reducers.jsx'; -import * as actionCreators from './action-creators.jsx'; -import VmsListing from './components/VmsListing.jsx'; -import { logDebug } from './utils.jsx'; -import { watchMetrics, cleanupMetricsWatch } from "./watch-metrics.es6"; - -import '../../../machines/machines.less'; // once per component hierarchy - -let reduxStore; -function initReduxStore() { - if (reduxStore) { - logDebug('initReduxStore(): store already initialized, skipping. ', reduxStore); - return; - } - logDebug('initReduxStore(): initializing empty store'); - const initialState = { - vms: [] - }; - - const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(); - reduxStore = createStore(reducers, initialState, storeEnhancers); -} - -function addKubeLoaderListener ($scope, kubeLoader, kubeSelect) { - // register load callback( callback, until ) - kubeLoader.listen(() => { - const vms = kubeSelect().kind('VirtualMachine'); - const persistentVolumes = kubeSelect().kind('PersistentVolume'); - const pods = kubeSelect().kind('Pod'); - - reduxStore.dispatch(actionCreators.setVms(Object.values(vms))); - reduxStore.dispatch(actionCreators.setPVs(Object.values(persistentVolumes))); - reduxStore.dispatch(actionCreators.setPods(Object.values(pods))); - }, $scope); - - // enable watching( watched-entity-type, until ) - kubeLoader.watch('VirtualMachine', $scope); - kubeLoader.watch('PersistentVolume', $scope); - kubeLoader.watch('Pod', $scope); -} - -const VmsPlugin = () => ( - - - -); - -function addScopeVarsToStore ($scope) { - $scope.$watch( - scope => scope.settings, - newSettings => reduxStore.dispatch(actionCreators.setSettings(newSettings))); -} - -/** - * - * @param {$rootScope.Scope} $scope 'VirtualMachinesCtrl' controller scope - * @param {kubeLoader} kubeLoader - * @param {kubeSelect} kubeSelect - * @param {kubeMethods} kubeMethods - * @param {KubeRequest} KubeRequest - */ -function init($scope, kubeLoader, kubeSelect, kubeMethods, KubeRequest) { - initReduxStore(); - addKubeLoaderListener($scope, kubeLoader, kubeSelect); - initMiddleware(kubeMethods, kubeLoader, KubeRequest); - addScopeVarsToStore($scope); - - watchMetrics(reduxStore); - $scope.$on("$destroy", () => cleanupMetricsWatch()); - - const rootElement = document.querySelector('#kubernetes-virtual-machines-root'); - React.render(, rootElement); -} - -export { init }; diff --git a/pkg/kubernetes/scripts/virtual-machines/selectors.jsx b/pkg/kubernetes/scripts/virtual-machines/selectors.jsx index 57560eea9888..5b2965abd021 100644 --- a/pkg/kubernetes/scripts/virtual-machines/selectors.jsx +++ b/pkg/kubernetes/scripts/virtual-machines/selectors.jsx @@ -24,7 +24,7 @@ import type { Vm } from './types.jsx'; * Returns pod corresponding to the given vm. */ export function getPod(vm, pods) { - if (!pods) { + if (!vm || !pods) { return null; } diff --git a/pkg/kubernetes/scripts/virtual-machines/store.es6 b/pkg/kubernetes/scripts/virtual-machines/store.es6 new file mode 100644 index 000000000000..54e03feb726f --- /dev/null +++ b/pkg/kubernetes/scripts/virtual-machines/store.es6 @@ -0,0 +1,47 @@ +/* + * This file is part of Cockpit. + * + * Copyright (C) 2017 Red Hat, Inc. + * + * Cockpit is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * Cockpit is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Cockpit; If not, see . + */ + +import { createStore } from 'redux'; +import reducers from './reducers.jsx'; +import { logDebug } from './utils.jsx'; + +let reduxStore; + +export function initStore () { + if (reduxStore) { + logDebug('initStore(): store already initialized, skipping.', reduxStore); + return reduxStore; + } + logDebug('initStore(): initializing empty store'); + const initialState = { + vms: [] + }; + + const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(); + reduxStore = createStore(reducers, initialState, storeEnhancers); + + return reduxStore; +} + +export function getStore () { + if (!reduxStore) { + logDebug('getStore(): store is not initialized yet.'); + } + return reduxStore; +} diff --git a/pkg/kubernetes/scripts/virtual-machines/utils.jsx b/pkg/kubernetes/scripts/virtual-machines/utils.jsx index 8913f1af92aa..3a358e470220 100644 --- a/pkg/kubernetes/scripts/virtual-machines/utils.jsx +++ b/pkg/kubernetes/scripts/virtual-machines/utils.jsx @@ -31,6 +31,10 @@ export function getPairs(object) { })); } +export function prefixedId(idPrefix, id) { + return idPrefix ? `${idPrefix}-${id}` : null; +} + export function vmIdPrefx(vm) { return `vm-${vm.metadata.name}`; } diff --git a/pkg/kubernetes/views/virtual-machine-page.html b/pkg/kubernetes/views/virtual-machine-page.html new file mode 100644 index 000000000000..3b67419505aa --- /dev/null +++ b/pkg/kubernetes/views/virtual-machine-page.html @@ -0,0 +1 @@ +
diff --git a/pkg/lib/cockpit-components-detail-page.jsx b/pkg/lib/cockpit-components-detail-page.jsx index b269e8f009eb..7187462f4872 100644 --- a/pkg/lib/cockpit-components-detail-page.jsx +++ b/pkg/lib/cockpit-components-detail-page.jsx @@ -33,7 +33,7 @@ const DetailPage = ({children}) => { {children}
); -} +}; const DetailPageRow = ({title, idPrefix, children}) => { // TODO use React.Fragment instead and remove 'className="detail-row"' once React 16 is merged @@ -45,12 +45,12 @@ const DetailPageRow = ({title, idPrefix, children}) => {
); -} +}; DetailPageRow.propTypes = { title: React.PropTypes.string, idPrefix: React.PropTypes.string, // row will have no elements with id if not specified -} +}; const DetailPageHeader = ({title, iconClass, navigateUpTitle, onNavigateUp, actions, idPrefix}) => { const icon = iconClass ? () : null; @@ -72,7 +72,7 @@ const DetailPageHeader = ({title, iconClass, navigateUpTitle, onNavigateUp, acti {navigateUp}
); -} +}; DetailPageHeader.propTypes = { title: React.PropTypes.string, @@ -81,10 +81,10 @@ DetailPageHeader.propTypes = { onNavigateUp: React.PropTypes.func, actions: React.PropTypes.object, idPrefix: React.PropTypes.string, // header will have no elements with id if not specified -} +}; module.exports = { DetailPage, DetailPageHeader, DetailPageRow, -} +}; diff --git a/test/verify/check-openshift b/test/verify/check-openshift index 0fe14b9069a4..9e089ee971b0 100755 --- a/test/verify/check-openshift +++ b/test/verify/check-openshift @@ -18,16 +18,14 @@ # You should have received a copy of the GNU Lesser General Public License # along with Cockpit; If not, see . -import parent -from testlib import * - import os -import unittest -import time -import sys import re +import sys +import time +import unittest from kubelib import * +from testlib import * # NOTE: Both TestOpenshift and TestRegistry are in this single file to # prevent them from being run concurrently. Both use a 'openshift' @@ -442,11 +440,8 @@ LABEL io.projectatomic.nulecule.atomicappversion="0.1.11" \ # kubevirt is not available (means missing in API), so link should not be present b.wait_not_present("#vms-menu-link") - def testKubevirtMachinesList(self): - self.bootstrapKubevirt() - + def startKubevirtVm(self): o = self.openshift - # The "fedoravm" VirtualMachine is created from OfflineVirtualMachine o.execute("oc patch offlinevirtualmachine fedoravm --type=merge -p '{\"spec\":{\"running\": true}}'") # let's start the VM print("starting vm, might take a while") @@ -454,56 +449,172 @@ LABEL io.projectatomic.nulecule.atomicappversion="0.1.11" \ wait(lambda: "virt-launcher-fedoravm" in o.execute("oc get pods"), delay=2) wait(lambda: "Running" in o.execute("oc get pods | grep virt-launcher-fedoravm"), delay=2, tries=150) # Example: virt-launcher-fedoravm-----rx59t + def verifyBrowserLocation(self, url): + self.browser.wait_js_cond('window.location.href.slice({0}) === "{1}"'.format(-len(url), url)) + + def testKubevirtVmInstance(self): + self.bootstrapKubevirt() + self.startKubevirtVm() + o = self.openshift + b = self.browser + vm_instance_validator = self.VmInstanceValidator(self) + self.login_and_go("/kubernetes") + + with self.browser.wait_timeout(120): + b.wait_present("#vms-menu-link") + b.click("#vms-menu-link") + + # test navigation between the vm and vms + vms_url_location = "/kubernetes/index.html#/vms" + vm_name = "fedoravm" + vm_namespace = "kubevirt" + + vm_namespace_hash = "#/vms/{0}".format(vm_namespace) + vm_url_hash = "{0}/{1}".format(vm_namespace_hash, vm_name) + vm_url_location = "/kubernetes/index.html{0}".format(vm_url_hash) + + self.verifyBrowserLocation(vms_url_location) + b.wait_present("tr[data-row-id='vm-fedoravm']") + b.click("tr[data-row-id='vm-fedoravm']") + + self.verifyBrowserLocation(vm_url_location) + b.wait_present("#vm-header-link-up") + b.click("#vm-header-link-up") + self.verifyBrowserLocation(vms_url_location) + b.go(vm_url_hash) + self.verifyBrowserLocation(vm_url_location) + + # check all rows + b.wait_present("#vm-fedoravm-vm-detail-row-title") + b.wait_in_text("#vm-fedoravm-vm-detail-row-title", "VM") + vm_instance_validator.validateOverview() + b.wait_present("#vm-fedoravm-usage-detail-row-title") + b.wait_in_text("#vm-fedoravm-usage-detail-row-title", "Usage") + vm_instance_validator.validateUsage() + b.wait_present("#vm-fedoravm-disks-detail-row-title") + b.wait_in_text("#vm-fedoravm-disks-detail-row-title", "Disks") + vm_instance_validator.validateDisks() + + # delete the VM (not the OVM), the VM pod should be recreated automatically + b.click("#vm-fedoravm-delete") + self.verifyBrowserLocation(vms_url_location) + # Since the VM is created from OVM, it will be automatically restarted (spec.running: true) + b.wait_not_present("tr[data-row-id='vm-fedoravm']") + b.wait_present("tr[data-row-id='vm-fedoravm']") + b.go(vm_url_hash) + + # check usage still not present because vm is not running yet) + vm_instance_validator.validateEmptyUsage() + + wait(lambda: "Running" in o.execute("oc get pods | grep virt-launcher-fedoravm"), delay=2, tries=150) + + # check links are present + with self.browser.wait_timeout(15): + b.wait_present("#vm-fedoravm-node") + wait(lambda: b.text("#vm-fedoravm-node > a").strip() != "-") + wait(lambda: b.is_present("#vm-fedoravm-pod > a") and b.text("#vm-fedoravm-pod > a").strip() != "") + b.wait_in_text("#vm-fedoravm-pod", "virt-launcher-fedoravm") + + # namespace redirect to vms + b.go(vm_namespace_hash) + self.verifyBrowserLocation(vms_url_location + "?namespace=kubevirt") + + # check invalid vm + invalid_vm_name = "invalidname" + invalid_vm_namespace = "invalidns" + invalid_vm_hash = "#/vms/{0}/{1}".format(invalid_vm_namespace, invalid_vm_name) + invalid_vm_url_location = "/kubernetes/index.html{0}".format(invalid_vm_hash) + b.go(invalid_vm_hash) + self.verifyBrowserLocation(invalid_vm_url_location) + b.wait_present("#vm-not-found-detail-row-title") + b.wait_in_text("#vm-not-found-detail-row-title", "VM {0}:{1} does not exist.".format(invalid_vm_namespace, invalid_vm_name)) + + class VmInstanceValidator: + def __init__(self, test_obj): + self.browser = test_obj.browser + self.openshift = test_obj.openshift + self.assertFalse = test_obj.assertFalse + self.assertGreaterEqual = test_obj.assertGreaterEqual + + def validateOverview(self): + b = self.browser + b.wait_present("#vm-fedoravm-memory") + b.wait_in_text("#vm-fedoravm-memory", "256M") + b.wait_in_text("#vm-fedoravm-labels", "kubevirt.io/nodeName=f1.cockpit.lan") + b.wait_present("#vm-fedoravm-pod") + + def validateUsage(self): + b = self.browser + + b.wait_present("#vm-fedoravm-cpu-metric div svg") # cannot check cpu component because it is an external dependency + b.wait_present("#vm-fedoravm-memory-metric") + b.wait_present("#vm-fedoravm-network-metric") + + wait(lambda: self._get_units_number("#vm-fedoravm-memory-metric-title") > 0) + self.assertGreaterEqual(self._get_units_number("#vm-fedoravm-download-metric", True), 0) + self.assertGreaterEqual(self._get_units_number("#vm-fedoravm-upload-metric", True), 0) + + def validateEmptyUsage(self): + b = self.browser + + wait(lambda: b.is_present("#vm-fedoravm-usage-metrics") and b.text( + "#vm-fedoravm-usage-metrics") == "Usage metrics are available after the pod starts", tries=120) + self.assertFalse(b.is_present("#vm-fedoravm-cpu-metric")) + self.assertFalse(b.is_present("#vm-fedoravm-memory-metric")) + self.assertFalse(b.is_present("#vm-fedoravm-network-metric")) + + def validateDisks(self): + b = self.browser + + b.wait_present("#vm-fedoravm-disks-disk0-device") # check disk properties + b.wait_present("#vm-fedoravm-disks-disk1-device") + b.wait_in_text("#vm-fedoravm-disks-disk0-device", "disk0") + b.wait_in_text("#vm-fedoravm-disks-disk1-device", "disk1") + b.wait_in_text("#vm-fedoravm-disks-disk0-bus", "virtio") + b.wait_in_text("#vm-fedoravm-disks-disk1-bus", "virtio") + b.wait_in_text("#vm-fedoravm-disks-disk0-source", "kubevirt/fedora-cloud-registry-disk-demo:latest") # registryvolume + b.wait_in_text("#vm-fedoravm-disks-disk1-source", "cloudinitvolume") + + # TODO: add iSCSI disk to the test VM and reenable here + # b.click("tr.listing-ct-item.listing-ct-noexpand") # test navigation to the "Volumes" page + # b.wait_js_cond('window.location.hash === "#/volumes/iscsi-disk-alpine"') + + def _get_units_number(self, selector, per_second=False): + units_regex = "([0-9]+\.?[0-9]*) .?i?B" + if per_second: + units_regex += "/s" + m = re.match(units_regex, self.browser.text(selector)) + return float(m.group(1)) if m else None + + + def testKubevirtMachinesList(self): + self.bootstrapKubevirt() + self.startKubevirtVm() + o = self.openshift b = self.browser self.login_and_go("/kubernetes") # kubevirt is available, so link should be present - b.wait_present("#vms-menu-link") + with self.browser.wait_timeout(120): + b.wait_present("#vms-menu-link") b.click("#vms-menu-link") b.wait_present("tr[data-row-id='vm-fedoravm']") self.assertEqual(b.text("tr[data-row-id='vm-fedoravm'] th"), "fedoravm") + vm_instance_validator = self.VmInstanceValidator(self) # expand row and check the Overview subtab - b.click("tr[data-row-id='vm-fedoravm']") + b.click("tr[data-row-id='vm-fedoravm'] td.listing-ct-toggle") b.wait_present("tr[data-row-id='vm-fedoravm'] + tr.listing-ct-panel") b.wait_in_text("tr[data-row-id='vm-fedoravm'] + tr.listing-ct-panel", "Node") - b.wait_present("#vm-fedoravm-memory") - b.wait_in_text("#vm-fedoravm-memory", "256M") - b.wait_in_text("#vm-fedoravm-labels", "kubevirt.io/nodeName=f1.cockpit.lan") - b.wait_present("#vm-fedoravm-pod") - + vm_instance_validator.validateOverview() # switch to the Usage subtab b.wait_present("#vm-fedoravm-usage-tab") b.click("#vm-fedoravm-usage-tab") - b.wait_present("#cpu-metric div svg") # cannot check cpu component because it is an external dependency - b.wait_present("#memory-metric") - b.wait_present("#network-metric") - - def get_units_value(selector, per_second=False): - units_regex = "([0-9]+\.?[0-9]*) .?i?B" - if per_second: - units_regex += "/s" - m = re.match(units_regex, b.text(selector)) - return float(m.group(1)) if m else None - - wait(lambda: get_units_value("#memory-metric-title") > 0) - self.assertGreaterEqual(get_units_value("#download-metric", True), 0) - self.assertGreaterEqual(get_units_value("#upload-metric", True), 0) + vm_instance_validator.validateUsage() # switch to the Disks subtab b.wait_present("#vm-fedoravm-disks-tab") b.click("#vm-fedoravm-disks-tab") - b.wait_present("#vm-fedoravm-disks-disk0-device") # check disk properties - b.wait_present("#vm-fedoravm-disks-disk1-device") - b.wait_in_text("#vm-fedoravm-disks-disk0-device", "disk0") - b.wait_in_text("#vm-fedoravm-disks-disk1-device", "disk1") - b.wait_in_text("#vm-fedoravm-disks-disk0-bus", "virtio") - b.wait_in_text("#vm-fedoravm-disks-disk1-bus", "virtio") - b.wait_in_text("#vm-fedoravm-disks-disk0-source", "kubevirt/fedora-cloud-registry-disk-demo:latest") # registryvolume - b.wait_in_text("#vm-fedoravm-disks-disk1-source", "cloudinitvolume") - - # TODO: add iSCSI disk to the test VM and reenable here - # b.click("tr.listing-ct-item.listing-ct-noexpand") # test navigation to the "Volumes" page - # b.wait_js_cond('window.location.hash === "#/volumes/iscsi-disk-alpine"') + vm_instance_validator.validateDisks() # return back to the virtual-machines b.wait_present("#vms-menu-link") # left-side menu @@ -521,29 +632,27 @@ LABEL io.projectatomic.nulecule.atomicappversion="0.1.11" \ b.wait_present("tr[data-row-id='vm-fedoravm']") if not b.is_present("#vm-fedoravm-usage-tab"): # if not expanded - b.click("tr[data-row-id='vm-fedoravm']") # expand row + b.click("tr[data-row-id='vm-fedoravm'] td.listing-ct-toggle") # expand row # check usage still not present because vm is not running yet) b.wait_present("#vm-fedoravm-usage-tab") b.click("#vm-fedoravm-usage-tab") - b.wait_present("#usage-metrics") - b.wait_in_text("#usage-metrics", "Usage metrics are available after the pod starts") - self.assertFalse(b.is_present("#cpu-metric")) - self.assertFalse(b.is_present("#memory-metric")) - self.assertFalse(b.is_present("#network-metric")) + vm_instance_validator.validateEmptyUsage() + + b.wait_present("#vm-fedoravm-overview-tab") b.click("#vm-fedoravm-overview-tab") - b.click("tr[data-row-id='vm-fedoravm']") # hide row + b.click("tr[data-row-id='vm-fedoravm'] td.listing-ct-toggle") # hide row wait(lambda: "Running" in o.execute("oc get pods | grep virt-launcher-fedoravm"), delay=2, tries=150) # link to node b.wait_present("tr[data-row-id='vm-fedoravm']") # the just-started VM is listed - b.click("tr[data-row-id='vm-fedoravm']") # expand row + b.click("tr[data-row-id='vm-fedoravm'] td.listing-ct-toggle") # expand row b.wait_present("tr[data-row-id='vm-fedoravm'] + tr.listing-ct-panel") b.wait_present("#vm-fedoravm-node") wait(lambda: b.text("#vm-fedoravm-node > a").strip() != "-") # `-` == unassigned ; so wait for a change node_name = b.text("#vm-fedoravm-node > a").strip() b.click("#vm-fedoravm-node > a") # jump to "node" detail - b.wait_js_cond('window.location.hash === "#/nodes/%s"' % (node_name,)) + self.verifyBrowserLocation('/kubernetes/index.html#/nodes/%s' % node_name) # link to pod b.click("#vms-menu-link") @@ -552,7 +661,7 @@ LABEL io.projectatomic.nulecule.atomicappversion="0.1.11" \ b.wait_in_text("#vm-fedoravm-pod", "virt-launcher-fedoravm") pod_name = b.text("#vm-fedoravm-pod > a").strip() b.click("#vm-fedoravm-pod > a") - b.wait_js_cond('window.location.hash === "#/l/pods/kubevirt/%s"' % (pod_name)) + self.verifyBrowserLocation('/kubernetes/index.html#/l/pods/kubevirt/%s' % pod_name) def testKubevirtMachinesCreate(self): class TestCreateConfig: @@ -582,7 +691,7 @@ LABEL io.projectatomic.nulecule.atomicappversion="0.1.11" \ self.assertEqual(b.text("{0} td:nth-of-type(2)".format(row_selector)), dialog.getNamespace()) # expand row - b.click(row_selector) + b.click("{0} td.listing-ct-toggle".format(row_selector)) b.wait_present("{0} + tr.listing-ct-panel".format(row_selector)) b.wait_in_text("{0} + tr.listing-ct-panel".format(row_selector), "Node") @@ -591,7 +700,8 @@ LABEL io.projectatomic.nulecule.atomicappversion="0.1.11" \ def deleteVm(self, dialog): b = self.browser self.openshift.execute("oc delete vm {0} --namespace={1}".format(dialog.name, dialog.getNamespace())) - b.wait_not_present("tr[data-row-id='vm-{0}']".format(dialog.name)) + with self.browser.wait_timeout(300): + b.wait_not_present("tr[data-row-id='vm-{0}']".format(dialog.name)) return self