提交 4c8b3633 编写于 作者: L Lukasz Zajaczkowski

Implement logs menu for ReplicaSetList view

上级 560b4aad
......@@ -8,7 +8,7 @@
"angular": "~1.4.2",
"angular-animate": "~1.4.2",
"angular-aria": "~1.4.2",
"angular-material": "~1.0.0-rc6",
"angular-material": "~1.0.0",
"angular-messages": "~1.4.2",
"angular-ui-router": "~0.2.15",
"angular-resource": "~1.4.2",
......
......@@ -19,6 +19,7 @@ import (
restful "github.com/emicklei/go-restful"
client "k8s.io/kubernetes/pkg/client/unversioned"
"strconv"
)
// Creates a new HTTP handler that handles all requests to the API of the backend.
......@@ -161,7 +162,11 @@ func (apiHandler *ApiHandler) handleGetReplicaSetPods(
namespace := request.PathParameter("namespace")
replicaSet := request.PathParameter("replicaSet")
result, err := GetReplicaSetPods(apiHandler.client, namespace, replicaSet)
limit, err := strconv.Atoi(request.QueryParameter("limit"))
if err != nil {
limit = 0
}
result, err := GetReplicaSetPods(apiHandler.client, namespace, replicaSet, limit)
if err != nil {
handleInternalError(response, err)
return
......
......@@ -16,19 +16,25 @@ package main
import (
api "k8s.io/kubernetes/pkg/api"
types "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned"
client "k8s.io/kubernetes/pkg/client/unversioned"
"sort"
)
// TotalRestartCountSorter sorts ReplicaSetPodWithContainers by restarts number.
type TotalRestartCountSorter []ReplicaSetPodWithContainers
func (a TotalRestartCountSorter) Len() int { return len(a) }
func (a TotalRestartCountSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a TotalRestartCountSorter) Less(i, j int) bool {
return a[i].TotalRestartCount > a[j].TotalRestartCount
}
// Information about a Container that belongs to a Pod.
type PodContainer struct {
// Name of a Container.
Name string `json:"name"`
// Container state.
State types.ContainerState `json:"state,omitempty"`
// Number of restarts.
RestartCount int `json:"restartCount"`
}
......@@ -47,26 +53,32 @@ type ReplicaSetPodWithContainers struct {
// Time the Pod has started. Empty if not started.
StartTime *unversioned.Time `json:"startTime"`
// Total number of restarts.
TotalRestartCount int `json:"totalRestartCount"`
// List of Containers that belongs to particular Pod.
PodContainers []PodContainer `json:"podContainers"`
}
// Returns list of pods with containers for the given replica set in the given namespace.
func GetReplicaSetPods(client *client.Client, namespace string, name string) (
// Limit specify the number of records to return. There is no limit when given value is zero.
func GetReplicaSetPods(client *client.Client, namespace string, name string, limit int) (
*ReplicaSetPods, error) {
pods, err := getRawReplicaSetPods(client, namespace, name)
if err != nil {
return nil, err
}
return getReplicaSetPods(pods.Items), nil
return getReplicaSetPods(pods.Items, limit), nil
}
// Creates and return structure containing pods with containers for given replica set.
func getReplicaSetPods(pods []api.Pod) *ReplicaSetPods {
// Data is sorted by total number of restarts for replica set pod.
// Result set can be limited
func getReplicaSetPods(pods []api.Pod, limit int) *ReplicaSetPods {
replicaSetPods := &ReplicaSetPods{}
for _, pod := range pods {
totalRestartCount := 0
replicaSetPodWithContainers := ReplicaSetPodWithContainers{
Name: pod.Name,
StartTime: pod.Status.StartTime,
......@@ -75,13 +87,21 @@ func getReplicaSetPods(pods []api.Pod) *ReplicaSetPods {
podContainer := PodContainer{
Name: containerStatus.Name,
RestartCount: containerStatus.RestartCount,
State: containerStatus.State,
}
replicaSetPodWithContainers.PodContainers =
append(replicaSetPodWithContainers.PodContainers, podContainer)
totalRestartCount += containerStatus.RestartCount
}
replicaSetPodWithContainers.TotalRestartCount = totalRestartCount
replicaSetPods.Pods = append(replicaSetPods.Pods, replicaSetPodWithContainers)
}
sort.Sort(TotalRestartCountSorter(replicaSetPods.Pods))
if limit > 0 {
if limit > len(replicaSetPods.Pods) {
limit = len(replicaSetPods.Pods)
}
replicaSetPods.Pods = replicaSetPods.Pods[0:limit]
}
return replicaSetPods
}
......@@ -148,3 +148,28 @@ backendApi.NamespaceList;
* }}
*/
backendApi.Label;
/**
* @typedef {{
* name: string,
* restartCount: number
* }}
*/
backendApi.PodContainer;
/**
* @typedef {{
* name: string,
* startTime: string,
* totalRestartCount: number,
* podContainers: !Array<!backendApi.PodContainer>
* }}
*/
backendApi.ReplicaSetPodWithContainers;
/**
* @typedef {{
* pods: !Array<!backendApi.ReplicaSetPodWithContainers>
* }}
*/
backendApi.ReplicaSetPods;
......@@ -26,4 +26,5 @@
.kd-content, body {
background-color: #eeeeee;
height: 100%;
}
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* Returns filter function to apply middle ellipsis when string is longer then max parameter.
* @return {Function}
*/
export default function middleEllipsisFilter() {
/**
* Filter function to apply middle ellipsis when string is longer then max parameter.
* @param {string} value Filtered value.
* @param {number} limit Length limit for filtered value.
* @return {string}
*/
let filterFunction = function(value, limit) {
limit = parseInt(limit, 10);
if (!limit) return value;
if (!value) return value;
if (value.length <= limit) return value;
if (limit === 1) return `${value[0]}...`;
/**
* Begin part of truncated text.
* @type {number}
*/
let beginPartIndex = Math.floor(limit / 2 + limit % 2);
/**
* End part of truncated text.
* @type {number}
*/
let endPartIndex = -Math.floor(limit / 2);
let begin = value.substring(0, beginPartIndex);
let end = value.slice(endPartIndex);
return `${begin}...${end}`;
};
return filterFunction;
}
<!--
Copyright 2015 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-menu>
<md-button ng-click="ctrl.openMenu($mdOpenMenu, $event)" class="kd-replicaset-card-logs">Logs</md-button>
<md-menu-content width="6">
<md-menu-item class="kd-menu-logs-md-menu-item">
<div flex="33">Logs</div>
</md-menu-item>
<md-menu-item class="kd-menu-logs-md-menu-item">
<div flex class="kd-menu-logs-item-header">Pod</div>
<div flex="50" class="kd-menu-logs-item-header">Running since</div>
<div flex class="kd-menu-logs-item-header">Prior restart</div>
</md-menu-item>
<md-menu-item ng-repeat="pod in ctrl.replicaSetPodsList" class="kd-menu-logs-md-menu-item">
<div flex class="kd-menu-logs-item">{{pod.name | middleEllipsis:10}}</div>
<div flex="50" class="kd-menu-logs-item">
<a>
{{pod.startTime}}<i
class="material-icons kd-menu-logs-link-icon">open_in_new</i>
</a>
</div>
<div flex class="kd-menu-logs-item" ng-if="pod.totalRestartCount != 0">
<a>
Logs<i class="material-icons kd-menu-logs-link-icon">open_in_new</i>
</a>
</div>
<div flex class="kd-menu-logs-item" ng-if="pod.totalRestartCount == 0">
-
</div>
</md-menu-item>
</md-menu-content>
</md-menu>
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* Controller for the logs menu view.
*
* @final
*/
export default class LogsMenuController {
/**
* @param {!angular.$log} $log
* @param {!angular.$resource} $resource
* @ngInject
*/
constructor($log, $resource) {
/** @private {!angular.$resource} */
this.resource_ = $resource;
/** @private {!angular.$log} */
this.log_ = $log;
/**
* This is initialized from the scope.
* @export {string}
*/
this.replicaSetName;
/**
* This is initialized from the scope.
* @export {string}
*/
this.namespace;
/**
* This is initialized on open menu.
* @export {!Array<!backendApi.ReplicaSetPodWithContainers>}
*/
this.replicaSetPodsList;
}
/**
* Opens menu with pods and link to logs.
* @param {!function(MouseEvent)} $mdOpenMenu
* @param {!MouseEvent} $event
* @export
*/
openMenu($mdOpenMenu, $event) {
// This is needed to resolve problem with data refresh.
// Sometimes old data was included to the new one for a while.
if (this.replicaSetPodsList) {
this.replicaSetPodsList = [];
}
this.getReplicaSetPods_();
$mdOpenMenu($event);
}
/**
* @private
*/
getReplicaSetPods_() {
/** @type {!angular.Resource<!backendApi.ReplicaSetPods>} */
let resource =
this.resource_(`/api/replicasets/pods/${this.namespace}/${this.replicaSetName}?limit=10`);
resource.get(
(replicaSetPods) => {
this.log_.info('Successfully fetched Replica Set pods: ', replicaSetPods);
this.replicaSetPodsList = replicaSetPods.pods;
},
(err) => { this.log_.error('Error fetching Replica Set pods: ', err); });
}
}
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import LogsMenuController from './logsmenu_controller';
/**
* Returns directive definition object for logs menu.
* @return {!angular.Directive}
*/
export default function logsMenuDirective() {
return {
scope: {},
bindToController: {
'namespace': '=',
'replicaSetName': '=',
},
controller: LogsMenuController,
controllerAs: 'ctrl',
templateUrl: 'replicasetlist/logsmenu.html',
};
}
......@@ -38,7 +38,9 @@ limitations under the License.
<span flex="60">
{{::replicaSet.podsRunning}} pods running, {{::replicaSet.podsPending}} pending
</span>
<a flex="40" href="#" class="kd-replicaset-card-logs">Logs</a>
<logs-menu flex="40" namespace="::replicaSet.namespace"
replica-set-name="::replicaSet.name">
</logs-menu>
</div>
<hr class="kd-replicaset-card-divider"></hr>
<div layout="row" layout-wrap>
......
......@@ -19,16 +19,22 @@
.kd-replicaset-card-logs {
color: inherit;
text-decoration: none;
/* This override button style to make it look like a link */
padding: inherit;
margin: inherit;
min-width: inherit;
min-height: inherit;
line-height: 0px;
border-radius: inherit;
font-size: inherit;
font-weight: inherit;
text-transform: inherit;
}
.kd-replicaset-card-logs:after {
content: '\25BC';
}
.kd-replicaset-card-logs:hover {
text-decoration: underline;
}
.kd-replicaset-card-divider {
border: 0;
border-top: 1px solid;
......@@ -47,3 +53,25 @@
.kd-replicaset-card-label {
margin-right: 1em;
}
.kd-menu-logs-md-menu-item {
min-height: 30px;
height: 30px;
}
.kd-menu-logs-item-header {
color: rgba(0, 0, 0, 0.5);
font-size: 0.8em;
white-space: nowrap;
}
.kd-menu-logs-item {
white-space: nowrap;
overflow: hidden;
font-size: 0.8em;
}
.kd-menu-logs-link-icon {
font-size: 1em;
margin-left: 5px;
}
......@@ -13,6 +13,8 @@
// limitations under the License.
import stateConfig from './replicasetlist_state';
import logsMenuDirective from './logsmenu_directive';
import middleEllipsisFilter from './../common/filters/middleellipsis_filter';
/**
* Angular module for the Replica Set list view.
......@@ -23,6 +25,9 @@ export default angular.module(
'kubernetesDashboard.replicaSetList',
[
'ngMaterial',
'ngResource',
'ui.router',
])
.config(stateConfig);
.config(stateConfig)
.filter('middleEllipsis', middleEllipsisFilter)
.directive('logsMenu', logsMenuDirective);
......@@ -22,45 +22,59 @@ import (
func TestGetReplicaSetPods(t *testing.T) {
cases := []struct {
pods []api.Pod
expected *ReplicaSetPods
}{
{nil, &ReplicaSetPods{}},
{[]api.Pod{
{
ObjectMeta: api.ObjectMeta{
Name: "pod-1",
pods := []api.Pod{
{
ObjectMeta: api.ObjectMeta{
Name: "pod-1",
},
Status: api.PodStatus{
ContainerStatuses: []api.ContainerStatus{
{
Name: "container-1",
RestartCount: 0,
},
{
Name: "container-2",
RestartCount: 2,
},
},
Status: api.PodStatus{
ContainerStatuses: []api.ContainerStatus{
{
Name: "container-1",
RestartCount: 0,
},
{
Name: "container-2",
RestartCount: 2,
},
},
},
{
ObjectMeta: api.ObjectMeta{
Name: "pod-2",
},
Status: api.PodStatus{
ContainerStatuses: []api.ContainerStatus{
{
Name: "container-3",
RestartCount: 10,
},
},
},
},
}
cases := []struct {
pods []api.Pod
limit int
expected *ReplicaSetPods
}{
{nil, 0, &ReplicaSetPods{}},
{pods, 10, &ReplicaSetPods{Pods: []ReplicaSetPodWithContainers{
{
ObjectMeta: api.ObjectMeta{
Name: "pod-2",
},
Status: api.PodStatus{
ContainerStatuses: []api.ContainerStatus{
{
Name: "container-3",
RestartCount: 10,
},
Name: "pod-2",
TotalRestartCount: 10,
PodContainers: []PodContainer{
{
Name: "container-3",
RestartCount: 10,
},
},
},
}, &ReplicaSetPods{Pods: []ReplicaSetPodWithContainers{
{
Name: "pod-1",
Name: "pod-1",
TotalRestartCount: 2,
PodContainers: []PodContainer{
{
Name: "container-1",
......@@ -72,8 +86,12 @@ func TestGetReplicaSetPods(t *testing.T) {
},
},
},
}},
},
{pods, 1, &ReplicaSetPods{Pods: []ReplicaSetPodWithContainers{
{
Name: "pod-2",
Name: "pod-2",
TotalRestartCount: 10,
PodContainers: []PodContainer{
{
Name: "container-3",
......@@ -85,7 +103,7 @@ func TestGetReplicaSetPods(t *testing.T) {
},
}
for _, c := range cases {
actual := getReplicaSetPods(c.pods)
actual := getReplicaSetPods(c.pods, c.limit)
if !reflect.DeepEqual(actual, c.expected) {
t.Errorf("getReplicaSetPods(%#v) == %#v, expected %#v",
c.pods, actual, c.expected)
......
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import replicasetlistModule from 'replicasetlist/replicasetlist_module';
describe('Apply ellipsis filter', () => {
const testedString = 'podName';
beforeEach(function() { angular.mock.module(replicasetlistModule.name); });
it('has a applyMiddleEllipsis filter',
angular.mock.inject(function($filter) { expect($filter('middleEllipsis')).not.toBeNull(); }));
it("should return the same value if max parameter is undefined",
angular.mock.inject(function(middleEllipsisFilter) {
expect(middleEllipsisFilter(testedString)).toEqual('podName');
}));
it("should return the same value if length less then given max length parameter",
angular.mock.inject(function(middleEllipsisFilter) {
expect(middleEllipsisFilter(testedString, 10)).toEqual('podName');
}));
it("should return the same value when max = 0",
angular.mock.inject(function(middleEllipsisFilter) {
expect(middleEllipsisFilter(testedString, 0)).toEqual('podName');
}));
it("should return truncated value with ellipsis as tail",
angular.mock.inject(function(middleEllipsisFilter) {
expect(middleEllipsisFilter(testedString, 1)).toEqual('p...');
}));
it("should return truncated value with the ellipsis in the middle",
angular.mock.inject(function(middleEllipsisFilter) {
expect(middleEllipsisFilter(testedString, 2)).toEqual('p...e');
}));
it("should return truncated value with the ellipsis in the middle",
angular.mock.inject(function(middleEllipsisFilter) {
expect(middleEllipsisFilter(testedString, 3)).toEqual('po...e');
}));
it("should return truncated value with the ellipsis in the middle",
angular.mock.inject(function(middleEllipsisFilter) {
expect(middleEllipsisFilter(testedString, 5)).toEqual('pod...me');
}));
});
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import LogsMenuController from 'replicasetlist/logsmenu_controller';
import replicaSetListModule from 'replicasetlist/replicasetlist_module';
describe('Logs menu controller', () => {
/**
* Logs menu controller.
* @type {!LogsMenuController}
*/
let ctrl;
/**
* @type {!function()} mdOpenMenu
*/
let mdOpenMenu = function() {};
beforeEach(() => {
angular.mock.module(replicaSetListModule.name);
angular.mock.inject(($controller) => { ctrl = $controller(LogsMenuController); });
});
it('should instantiate the controller properly', () => { expect(ctrl).not.toBeUndefined(); });
it('should clear replicaSetPodsList on open menu', () => {
ctrl.replicaSetPodsList = [
{
"name": "frontend-i0vvd",
"startTime": "2015-12-08T09:00:34Z",
"totalRestartCount": 0,
"podContainers": [
{
"name": "php-redis",
"restartCount": 0,
},
],
},
];
// when
ctrl.openMenu(mdOpenMenu);
// then
expect(ctrl.replicaSetPodsList).toEqual([]);
});
});
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册