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

Implement logs menu for ReplicaSetList view

上级 560b4aad
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
"angular": "~1.4.2", "angular": "~1.4.2",
"angular-animate": "~1.4.2", "angular-animate": "~1.4.2",
"angular-aria": "~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-messages": "~1.4.2",
"angular-ui-router": "~0.2.15", "angular-ui-router": "~0.2.15",
"angular-resource": "~1.4.2", "angular-resource": "~1.4.2",
......
...@@ -19,6 +19,7 @@ import ( ...@@ -19,6 +19,7 @@ import (
restful "github.com/emicklei/go-restful" restful "github.com/emicklei/go-restful"
client "k8s.io/kubernetes/pkg/client/unversioned" client "k8s.io/kubernetes/pkg/client/unversioned"
"strconv"
) )
// Creates a new HTTP handler that handles all requests to the API of the backend. // Creates a new HTTP handler that handles all requests to the API of the backend.
...@@ -161,7 +162,11 @@ func (apiHandler *ApiHandler) handleGetReplicaSetPods( ...@@ -161,7 +162,11 @@ func (apiHandler *ApiHandler) handleGetReplicaSetPods(
namespace := request.PathParameter("namespace") namespace := request.PathParameter("namespace")
replicaSet := request.PathParameter("replicaSet") 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 { if err != nil {
handleInternalError(response, err) handleInternalError(response, err)
return return
......
...@@ -16,19 +16,25 @@ package main ...@@ -16,19 +16,25 @@ package main
import ( import (
api "k8s.io/kubernetes/pkg/api" api "k8s.io/kubernetes/pkg/api"
types "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/unversioned"
client "k8s.io/kubernetes/pkg/client/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. // Information about a Container that belongs to a Pod.
type PodContainer struct { type PodContainer struct {
// Name of a Container. // Name of a Container.
Name string `json:"name"` Name string `json:"name"`
// Container state.
State types.ContainerState `json:"state,omitempty"`
// Number of restarts. // Number of restarts.
RestartCount int `json:"restartCount"` RestartCount int `json:"restartCount"`
} }
...@@ -47,26 +53,32 @@ type ReplicaSetPodWithContainers struct { ...@@ -47,26 +53,32 @@ type ReplicaSetPodWithContainers struct {
// Time the Pod has started. Empty if not started. // Time the Pod has started. Empty if not started.
StartTime *unversioned.Time `json:"startTime"` StartTime *unversioned.Time `json:"startTime"`
// Total number of restarts.
TotalRestartCount int `json:"totalRestartCount"`
// List of Containers that belongs to particular Pod. // List of Containers that belongs to particular Pod.
PodContainers []PodContainer `json:"podContainers"` PodContainers []PodContainer `json:"podContainers"`
} }
// Returns list of pods with containers for the given replica set in the given namespace. // 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) { *ReplicaSetPods, error) {
pods, err := getRawReplicaSetPods(client, namespace, name) pods, err := getRawReplicaSetPods(client, namespace, name)
if err != nil { if err != nil {
return nil, err 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. // 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{} replicaSetPods := &ReplicaSetPods{}
for _, pod := range pods { for _, pod := range pods {
totalRestartCount := 0
replicaSetPodWithContainers := ReplicaSetPodWithContainers{ replicaSetPodWithContainers := ReplicaSetPodWithContainers{
Name: pod.Name, Name: pod.Name,
StartTime: pod.Status.StartTime, StartTime: pod.Status.StartTime,
...@@ -75,13 +87,21 @@ func getReplicaSetPods(pods []api.Pod) *ReplicaSetPods { ...@@ -75,13 +87,21 @@ func getReplicaSetPods(pods []api.Pod) *ReplicaSetPods {
podContainer := PodContainer{ podContainer := PodContainer{
Name: containerStatus.Name, Name: containerStatus.Name,
RestartCount: containerStatus.RestartCount, RestartCount: containerStatus.RestartCount,
State: containerStatus.State,
} }
replicaSetPodWithContainers.PodContainers = replicaSetPodWithContainers.PodContainers =
append(replicaSetPodWithContainers.PodContainers, podContainer) append(replicaSetPodWithContainers.PodContainers, podContainer)
totalRestartCount += containerStatus.RestartCount
} }
replicaSetPodWithContainers.TotalRestartCount = totalRestartCount
replicaSetPods.Pods = append(replicaSetPods.Pods, replicaSetPodWithContainers) 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 return replicaSetPods
} }
...@@ -148,3 +148,28 @@ backendApi.NamespaceList; ...@@ -148,3 +148,28 @@ backendApi.NamespaceList;
* }} * }}
*/ */
backendApi.Label; 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 @@ ...@@ -26,4 +26,5 @@
.kd-content, body { .kd-content, body {
background-color: #eeeeee; 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. ...@@ -38,7 +38,9 @@ limitations under the License.
<span flex="60"> <span flex="60">
{{::replicaSet.podsRunning}} pods running, {{::replicaSet.podsPending}} pending {{::replicaSet.podsRunning}} pods running, {{::replicaSet.podsPending}} pending
</span> </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> </div>
<hr class="kd-replicaset-card-divider"></hr> <hr class="kd-replicaset-card-divider"></hr>
<div layout="row" layout-wrap> <div layout="row" layout-wrap>
......
...@@ -19,16 +19,22 @@ ...@@ -19,16 +19,22 @@
.kd-replicaset-card-logs { .kd-replicaset-card-logs {
color: inherit; color: inherit;
text-decoration: none; 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 { .kd-replicaset-card-logs:after {
content: '\25BC'; content: '\25BC';
} }
.kd-replicaset-card-logs:hover {
text-decoration: underline;
}
.kd-replicaset-card-divider { .kd-replicaset-card-divider {
border: 0; border: 0;
border-top: 1px solid; border-top: 1px solid;
...@@ -47,3 +53,25 @@ ...@@ -47,3 +53,25 @@
.kd-replicaset-card-label { .kd-replicaset-card-label {
margin-right: 1em; 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 @@ ...@@ -13,6 +13,8 @@
// limitations under the License. // limitations under the License.
import stateConfig from './replicasetlist_state'; 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. * Angular module for the Replica Set list view.
...@@ -23,6 +25,9 @@ export default angular.module( ...@@ -23,6 +25,9 @@ export default angular.module(
'kubernetesDashboard.replicaSetList', 'kubernetesDashboard.replicaSetList',
[ [
'ngMaterial', 'ngMaterial',
'ngResource',
'ui.router', 'ui.router',
]) ])
.config(stateConfig); .config(stateConfig)
.filter('middleEllipsis', middleEllipsisFilter)
.directive('logsMenu', logsMenuDirective);
...@@ -22,12 +22,7 @@ import ( ...@@ -22,12 +22,7 @@ import (
func TestGetReplicaSetPods(t *testing.T) { func TestGetReplicaSetPods(t *testing.T) {
cases := []struct { pods := []api.Pod{
pods []api.Pod
expected *ReplicaSetPods
}{
{nil, &ReplicaSetPods{}},
{[]api.Pod{
{ {
ObjectMeta: api.ObjectMeta{ ObjectMeta: api.ObjectMeta{
Name: "pod-1", Name: "pod-1",
...@@ -58,9 +53,28 @@ func TestGetReplicaSetPods(t *testing.T) { ...@@ -58,9 +53,28 @@ func TestGetReplicaSetPods(t *testing.T) {
}, },
}, },
}, },
}, &ReplicaSetPods{Pods: []ReplicaSetPodWithContainers{ }
cases := []struct {
pods []api.Pod
limit int
expected *ReplicaSetPods
}{
{nil, 0, &ReplicaSetPods{}},
{pods, 10, &ReplicaSetPods{Pods: []ReplicaSetPodWithContainers{
{
Name: "pod-2",
TotalRestartCount: 10,
PodContainers: []PodContainer{
{
Name: "container-3",
RestartCount: 10,
},
},
},
{ {
Name: "pod-1", Name: "pod-1",
TotalRestartCount: 2,
PodContainers: []PodContainer{ PodContainers: []PodContainer{
{ {
Name: "container-1", Name: "container-1",
...@@ -72,8 +86,12 @@ func TestGetReplicaSetPods(t *testing.T) { ...@@ -72,8 +86,12 @@ func TestGetReplicaSetPods(t *testing.T) {
}, },
}, },
}, },
}},
},
{pods, 1, &ReplicaSetPods{Pods: []ReplicaSetPodWithContainers{
{ {
Name: "pod-2", Name: "pod-2",
TotalRestartCount: 10,
PodContainers: []PodContainer{ PodContainers: []PodContainer{
{ {
Name: "container-3", Name: "container-3",
...@@ -85,7 +103,7 @@ func TestGetReplicaSetPods(t *testing.T) { ...@@ -85,7 +103,7 @@ func TestGetReplicaSetPods(t *testing.T) {
}, },
} }
for _, c := range cases { for _, c := range cases {
actual := getReplicaSetPods(c.pods) actual := getReplicaSetPods(c.pods, c.limit)
if !reflect.DeepEqual(actual, c.expected) { if !reflect.DeepEqual(actual, c.expected) {
t.Errorf("getReplicaSetPods(%#v) == %#v, expected %#v", t.Errorf("getReplicaSetPods(%#v) == %#v, expected %#v",
c.pods, actual, c.expected) 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.
先完成此消息的编辑!
想要评论请 注册