Skip to content

Add kubernetes style sidebar#1500

Merged
dadjeibaah merged 11 commits intomasterfrom
dad/add-k8s-sidebar
Aug 27, 2018
Merged

Add kubernetes style sidebar#1500
dadjeibaah merged 11 commits intomasterfrom
dad/add-k8s-sidebar

Conversation

@dadjeibaah
Copy link
Contributor

@dadjeibaah dadjeibaah commented Aug 21, 2018

Linkerd CLI's "look and feel" is similar to Kubernetes kubectl CLI. Linkerd's dashboard can be extended to match Kubernetes dashboard UI.

This PR serves as a starting point for this work. The new sidebar shows all resources from all namespaces on initial page load. Resources can be filtered to show only items in a given namespace. The sidebar displays authority, deployment, service and, pod resources. We may need to think about whether it is necessary to show all resources types. Some resources, i.e. authorities, contain a large cardinality of resource details and may not be very useful to a user.

screen shot 2018-08-23 at 5 29 12 pm

fixes #1449

Copy link

@rmars rmars left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! great start. if you merge master into this branch, the props logic in ResourceDetail might help with some of the problems you're seeing here.

left a couple comments below.

{
_.map(_.sortBy(sidebarComponents[resourceName], r => r.resource.name), ro => {
return (
<Menu.Item key={"/namespaces/" + ro.resource.namespace + "/" + ro.resource.type + "s/" + ro.resource.name}>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can increase readability by refactoring the "/namespaces/" + ro.resource.namespace + "/" + ro.resource.type + "s/" + ro.resource.name so we don't repeat this particular string so many times here

return {nsValue: ns, nsName: ns};
}));
let sidebarComponents = this.filterResourcesByNamespace(this.state.resourceGroupings, this.state.namespaceFilter);
const SubMenu = withRouter(Menu.SubMenu);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is causing a console error that is hiding the real problems :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♂️ Making things worse for myself. Thanks for catching that.

</PrefixedLink>
</Menu.Item>

<Menu.Item className="sidebar-menu-item" key="/namespaces">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we still want to access the list pages (/namespaces, /deployments, /pods) etc. from somewhere so let's make sure these don't get lost

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I wasn't sure whether those pages would be needed since the sidebar partially "becomes" the namespace page. Although, I do agree that we still want to be able to view stats about requests between namespaces since we lose visibility of these stats if we rely solely on the sidebar.

: null
}

<Menu.SubMenu
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should include some way to access these list pages in the nav

let namespaces = [{nsValue: "all", nsName: "All Namespaces"}].concat(_.map(this.state.namespaces, ns => {
return {nsValue: ns, nsName: ns};
}));
let sidebarComponents = this.filterResourcesByNamespace(this.state.resourceGroupings, this.state.namespaceFilter);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

authorities are kind of different from the other resources and it makes less sense to have this type of dashboard for them; also they're not linked in the web server routes to going to http://localhost:8084/namespaces/linkerd/authoritys/10.1.1.97:9995 won't go anywhere. We should filter out the resource types that we don't have resource detail pages for (or add routing for them in web/srv/server.go, currently there's only three resource types there)

<span>{ns} {this.state.collapsed ? "namespace" : ""}</span>
</PrefixedLink>
</Menu.Item>
<SubMenu
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if there are no sub items in the menu, we should either omit or disable linking for the high level component (e.g. there aren't any deployments when you select namespace kube-system and so the nave is misleading; it takes a lot of time to click on each resource type and figure out that there aren't resources in it)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I will get that set up on my next commit.

@dadjeibaah dadjeibaah force-pushed the dad/add-k8s-sidebar branch 2 times, most recently from 8ba3360 to e65881b Compare August 23, 2018 17:25
@dadjeibaah dadjeibaah changed the title [WIP] Add kubernetes style sidebar Add kubernetes style sidebar Aug 23, 2018
@dadjeibaah
Copy link
Contributor Author

There are a few things that have been mentioned in #1449 that I think should be in a separate PR. I am thinking this PR is a good starting point for the sidebar. I will create issues for the additional work needed to flesh the sidebar out.

Copy link

@rmars rmars left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍬 nice, this is coming along great!

import sinonStubPromise from "sinon-stub-promise";
import Enzyme, { mount } from "enzyme";


Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra spaces

.then(([versionRsp, allRsp, nsRsp]) => {
let statTables = _.get(allRsp, ["ok", "statTables"], []);
let rows = _.flatMap(statTables, tb => tb.podGroup.rows);
let resourceGroupings = _.groupBy(rows, r => r.resource.type);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can get this by using processMultiResourceRollup:

import { processMultiResourceRollup } from './util/MetricUtils.js';
...
let statsByResource = processMultiResourceRollup(allRsp)

The object format is a little different, so you'd be needing to do r.name instead of r.resource.name in all the places you do r.resource, for example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nice! this is great. Thanks for pointing that out.

let rows = _.flatMap(statTables, tb => tb.podGroup.rows);
let resourceGroupings = _.groupBy(rows, r => r.resource.type);
let nsStats = _.get(nsRsp, ["ok", "statTables", 0, "podGroup", "rows"], []);
let namespaces = _(nsStats).map(r => r.resource.name).sortBy().value();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize this is existing code, but we can also get this by using processSingleResourceRollup:

        let nsStats = processSingleResourceRollup(nsRsp);
        let namespaces = _(nsStats).map('name').sortBy().value();

{
_.map(namespaces, ns => {
return (
<Select.Option key={ns.nsValue} value={ns.nsValue}>{ns.nsName}</Select.Option>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ns.nsValue is a little repetitive, maybe ns.labelValue and ns.labelName? or simply ns.name and ns.value?
_.map(namespaces, label => ... label.name .... label.value might also be a clearer option

}

// Filters resources retrieved from a stat query by namespaces. Does not include resources of type "authority"
filterResourcesByNamespace(resources, namespace) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could get rid of the resources we don't want to show when we process the metrics in loadFromServer. This could just be delete resourceGroupings["authority"] there, for example. This removes the need for the deleting of it in this method.

I'll also note that services aren't wired in, so you might want to handle that too.

<Layout.Sider
width="260px"
breakpoint="lg"
collapsed={this.state.collapsed}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per discussion on the ticket, I think we'll want to retain the ability to collapse (or hide) the sidebar.

@rmars
Copy link

rmars commented Aug 23, 2018

Another comment I forgot to add was that I often see repeated resource names when "All Namespaces" is selected. I definitely think we should prefix the name with the namespace when on this view, otherwise it's confusing.

screen shot 2018-08-23 at 11 29 42 am

@dadjeibaah
Copy link
Contributor Author

yea, good point. Do you think its still ok to have the prefix when we are viewing resources from a single namespace?

@grampelberg
Copy link
Contributor

I actually don't mind the name duplication for all namespaces. I'd rather have the view identical between the filter selections than prefix only in this instance. It feels like a problem we're just creating for ourselves right now. That said, if @rmars feels particularly passionately about it, I don't see why we can't do it.

@dadjeibaah
Copy link
Contributor Author

screen shot 2018-08-23 at 2 37 16 pm

screen shot 2018-08-23 at 2 37 08 pm

IDK, I think I prefer having the namespace prefix. ^ Is what it looks like in both the unfiltered and filtered state.

@rmars
Copy link

rmars commented Aug 23, 2018

I really prefer the namespace prefix. If we don't want discrepancies between the views, I'd rather have it prefixed in all cases :)

@dadjeibaah dadjeibaah force-pushed the dad/add-k8s-sidebar branch from 60a4c2c to 30f9090 Compare August 24, 2018 05:28
Copy link

@rmars rmars left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sweeeet. the success rate dots look pretty cool!

import { linkerdLogoOnly, linkerdWordLogo } from './util/SvgWrappers.jsx';
import './../../css/sidebar.css';

const getSrClassification = sr => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have a similar method to this in like three places in the codebase now. I think we should try to consolidate this in a helper function in MetricUtils.js

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea good call. We probably should make the method accept a map that are associated status labels to good, ok and bad so we can use arbitrary css classNames.

};

export const excludeResourcesFromRollup = (rollupMetrics, resourceTypeList) => {
for (let i = 0; i < resourceTypeList.length; i++) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be more readable as

export const excludeResourcesFromRollup = (rollupMetrics, resourcesToExclude) => {
  _.each(resourcesToExclude, resource => {
    delete rollupMetrics[resource];
  })

@@ -32,6 +32,10 @@ ul.ant-menu.ant-menu-sub {
& .update {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Font feedback: looking at the sidebar we can see that the subitems are larger than the headings Deployments and Pods. Esp since the pod names are long, I think we should use a smaller font size (maybe 12px?)
screen shot 2018-08-24 at 2 00 20 pm

<Menu.SubMenu
className="sidebar-menu-item"
key={resourceName}
disabled={sidebarComponents[resourceName].length === 0}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: _.isEmpty(sidebarComponents[resourceName])

let PrefixedLink = this.api.PrefixedLink;
let numHiddenNamespaces = _.size(this.state.namespaces) - this.state.maxNsItemsToShow;

let namespaces = [{value: "all", name: "All Namespaces"}].concat(_.map(this.state.namespaces, ns => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readability nit:

    let namespaces = [
      {value: "all", name: "All Namespaces"}
    ].concat(_.map(this.state.namespaces, ns => {
      return { value: ns, name: ns };
    }));

return _.mapValues(resources, o => {
return _(o)
.filter(r => r.namespace === namespace && r.added)
.value();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we're only using one _ function here, there's no need to wrap o and then unwrap it with value... so this could be written as return _.filter(o, r => r.namespace === namespace && r.added)

this.setState({namespaceFilter: value});
}

// Filters resources retrieved from a stat query by namespaces. Does not include resources of type "authority"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this comment outdated?

if (namespace === "all") {
return _.mapValues(resources, o => {
return _(o)
.filter(r => r.added)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the only difference between these is this line... what if we refactored the repeated code by putting the filter into a named variable?

  filterResourcesByNamespace(resources, namespace) {
    let filter = namespace === "all" ? r => r.added :
      r => r.added && r.namespace === namespace;

    return _.mapValues(resources, res => _.filter(res, filter));
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slick! I like it!

{
_.map(_.sortBy(sidebarComponents[resourceName], r => `${r.namespace}/${r.name}`), r => {
// only display resources that have been meshed
if (r.added) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this if here if we've filtered out the unadded components in filterResourcesByNamespace?

to={this.generateResourceURL(r)}>
{`${r.namespace}/${r.name}`}
</PrefixedLink>
<Badge status={getSrClassification(r.successRate)} />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sweeeet

Dennis Adjei-Baah added 10 commits August 27, 2018 09:56
Signed-off-by: Dennis Adjei-Baah <[email protected]>
Signed-off-by: Dennis Adjei-Baah <[email protected]>
Signed-off-by: Dennis Adjei-Baah <[email protected]>
Signed-off-by: Dennis Adjei-Baah <[email protected]>
Signed-off-by: Dennis Adjei-Baah <[email protected]>
Signed-off-by: Dennis Adjei-Baah <[email protected]>
Signed-off-by: Dennis Adjei-Baah <[email protected]>
Signed-off-by: Dennis Adjei-Baah <[email protected]>
Signed-off-by: Dennis Adjei-Baah <[email protected]>
@dadjeibaah dadjeibaah force-pushed the dad/add-k8s-sidebar branch from 30f9090 to 116b14d Compare August 27, 2018 16:57
Copy link

@rmars rmars left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦈 🌟 thanks for adding this!

title={<span>{friendlyTitle(resourceName).plural}</span>}>
{
_.map(_.sortBy(sidebarComponents[resourceName], r => `${r.namespace}/${r.name}`), r => {
// only display resources that have been meshed`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: trailing `

});
};

const generateResourceURL = r => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sweeet! 🎉

return successRateLabels.bad;
} else if (rate < 0.95) {
return successRateLabels.neutral;
} else {return successRateLabels.good;}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacing nit:

else {
  return successRateLabels.good;
}

_.map(_.keys(sidebarComponents).sort(), resourceName => {
return (
<Menu.SubMenu
className="sidebar-menu-item"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIOLI: I think it would help usability to add a title={`${r.namespace}/${r.name}`} here so that the user can hover over and see the full name in case the name is truncated (probably not super important, but could be nice to have).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes total sense and I feel it is necessary. Thanks for pointing that out! 🙏

Signed-off-by: Dennis Adjei-Baah <[email protected]>
@dadjeibaah dadjeibaah merged commit 097632a into master Aug 27, 2018
@dadjeibaah dadjeibaah deleted the dad/add-k8s-sidebar branch August 27, 2018 19:59
<Menu.SubMenu
className="sidebar-menu-item"
key={resourceName}
title={<span>{friendlyTitle(resourceName).plural}</span>}>
Copy link

@rmars rmars Aug 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry I didn't catch this before, but title tags contain text only and ignore tags within the element.
https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/title

But also, unlike the pod names, I don't think there's any case where the full Resource Title will be cut off in the sidebar as is. So I think we should remove this title= entirely.

I can do that in a small branch of misc fixes I'm working on.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! thanks for catching that. I wasn't aware that the title prop was going to be the page title. I assumed it was something that antd used for displaying the Menu.SubMenu. Thanks for taking care of this!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kubernetes like navigation in the GUI

3 participants