Skip to content

Commit

Permalink
frontend: Convert namespaces page to separate list and details pages
Browse files Browse the repository at this point in the history
Switch the namespaces page from a 3 pane to separate list and details
page like the other resource types.

Change namespace search results column order from Name, Labels, Status
to Name, Status, Labels to match the new namespaces list page.
  • Loading branch information
kyoto committed Mar 30, 2017
1 parent 67666a7 commit 1f774a0
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 103 deletions.
7 changes: 0 additions & 7 deletions frontend/public/components/_namespace-sparklines.scss

This file was deleted.

6 changes: 6 additions & 0 deletions frontend/public/components/_sparkline.scss
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@
}
}

.co-namespace-sparkline {
padding-left: 0;
padding-right: 0;
max-width: 400px;
}

@keyframes sparkline-fade-in {
from {
opacity: 0;
Expand Down
7 changes: 5 additions & 2 deletions frontend/public/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { ErrorPage, ErrorPage404 } from './error';
import { EventStreamPage } from './events';
import { GlobalNotifications } from './global-notifications';
import { GlobalTooltip } from './global-tooltip';
import { NamespacesPage, NamespaceSelector } from './namespace';
import { NamespaceSelector } from './namespace';
import { Nav } from './nav';
import { ProfilePage } from './profile';
import { ResourceDetailsPage, ResourceListPage } from './resource-list';
Expand Down Expand Up @@ -97,7 +97,10 @@ render((
<Route path="ns/:ns/roles/:name/add-rule" component={EditRuleContainer} />
<Route path="ns/:ns/roles/:name/:rule/edit" component={EditRuleContainer} />

<Route path="namespaces" component={NamespacesPage} />
<Route path="namespaces">
<IndexRoute component={ResourceListPage} kind="namespaces" />
<Route path=":name/:view" component={ResourceDetailsPage} kind="namespaces" />
</Route>

<Route path="nodes">
<IndexRoute component={ResourceListPage} kind="nodes" />
Expand Down
8 changes: 5 additions & 3 deletions frontend/public/components/factory/list-page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ export class ListPage extends React.Component {
}

render () {
const {kind, namespace, ListComponent, dropdownFilters, rowFilters, filterLabel, showTitle = true, canExpand = false} = this.props;
const {kind, namespace, ListComponent, dropdownFilters, rowFilters, filterLabel, showTitle = true, canExpand = false, canCreate, createHandler} = this.props;
const {label, labelPlural, plural} = kindObj(kind);

const href = `ns/${namespace || k8sEnum.DefaultNS}/${plural}/new`;
const createProps = createHandler ? {onClick: createHandler} : {to: href};

const DropdownFilters = dropdownFilters && dropdownFilters.map(({type, items, title}) => {
return <Dropdown key={title} className="pull-right" items={items} title={title} onChange={this.onDropdownChange.bind(this, type)} />;
Expand All @@ -54,8 +56,8 @@ export class ListPage extends React.Component {
<div className="co-m-pane__heading">
<div className="row">
<div className="col-xs-12">
{ this.props.canCreate &&
<Link to={href} className="co-m-primary-action pull-left">
{ canCreate &&
<Link className="co-m-primary-action pull-left" {...createProps}>
<button className="btn btn-primary">
Create {label}
</button>
Expand Down
11 changes: 7 additions & 4 deletions frontend/public/components/modals/delete-namespace-modal.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';

import {k8s} from '../../module/k8s';
import {createModalLauncher, ModalTitle, ModalBody, ModalSubmitFooter} from '../factory/modal';
import {PromiseComponent} from '../utils';
import { k8s, k8sKinds } from '../../module/k8s';
import { createModalLauncher, ModalTitle, ModalBody, ModalSubmitFooter } from '../factory/modal';
import { history, PromiseComponent} from '../utils';

class DeleteNamespaceModal extends PromiseComponent {
constructor(props) {
Expand All @@ -20,7 +20,10 @@ class DeleteNamespaceModal extends PromiseComponent {

_submit(event) {
event.preventDefault();
this.handlePromise(k8s.namespaces.delete(this.props.resource)).then(this._close);
this.handlePromise(k8s.namespaces.delete(this.props.resource)).then(() => {
this._close();
history.push(k8sKinds.NAMESPACE.path);
});
}

render() {
Expand Down
136 changes: 60 additions & 76 deletions frontend/public/components/namespace.jsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,50 @@
import React from 'react';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import ReactTooltip from 'react-tooltip';

import {k8s} from '../module/k8s';
import {k8s, k8sEnum} from '../module/k8s';
import {actions, getActiveNamespace, isNamespaced} from '../ui/ui-actions';
import {makeList, TwoColumns} from './factory';
import {RowOfKind} from './RBAC/role';
import {DetailsPage, ListPage, makeList} from './factory';
import {SafetyFirst} from './safety-first';
import {SparklineWidget} from './sparkline-widget/sparkline-widget';
import {ActionsMenu, Cog, Dropdown, Firehose, kindObj, LabelList, LoadingInline, NavTitle, pluralize, ResourceIcon} from './utils';
import {Cog, Dropdown, Firehose, LabelList, LoadingInline, navFactory, ResourceCog, ResourceHeading, ResourceLink, ResourceSummary} from './utils';
import {createNamespaceModal, deleteNamespaceModal, configureNamespacePullSecretModal} from './modals';

const FullHeader = () => <div className="row co-m-table-grid__head">
<div className="col-xs-4">Namespace Name</div>
<div className="col-xs-4">Namespace Labels</div>
const deleteModal = (kind, ns) => {
let {label, weight} = Cog.factory.Delete(kind, ns);
let callback = undefined;
if (ns.metadata.name === k8sEnum.DefaultNS) {
label = <div className="dropdown__disabled" data-tip={`Namespace "${k8sEnum.DefaultNS}" cannot be deleted`}>{label}</div>;
} else if (ns.status.phase === 'Terminating') {
label = <div className="dropdown__disabled" data-tip="Namespace is already terminating">{label}</div>;
} else {
callback = () => deleteNamespaceModal({resource: ns});
}
return {label, weight, callback};
};

const menuActions = [Cog.factory.ModifyLabels, Cog.factory.ModifyAnnotations, deleteModal];

const Header = () => <div className="row co-m-table-grid__head">
<div className="col-xs-4">Name</div>
<div className="col-xs-4">Status</div>
<div className="col-xs-4">Labels</div>
</div>;

const FullRow = ({obj: namespace}) => <div className="row co-resource-list__item">
const Row = ({obj: ns}) => <div className="row co-resource-list__item">
<div className="col-xs-4">
<ResourceIcon kind="namespace" />
<Link to={`namespaces?name=${namespace.metadata.name}`} title={namespace.metadata.uid}>
{namespace.metadata.name}
</Link>
<ResourceCog actions={menuActions} kind="namespace" resource={ns} />
<ResourceLink kind="namespace" name={ns.metadata.name} title={ns.metadata.uid} />
</div>
<div className="col-xs-4">
<LabelList kind="namespace" labels={namespace.metadata.labels} />
{ns.status.phase}
</div>
<div className="col-xs-4">
{namespace.status.phase}
<LabelList kind="namespace" labels={ns.metadata.labels} />
</div>
</div>;

export const NamespacesList = makeList('Namespaces', 'namespace', FullHeader, FullRow);
export const NamespacesList = makeList('Namespaces', 'namespace', Header, Row);
export const NamespacesPage = props => <ListPage {...props} ListComponent={NamespacesList} canCreate={true} createHandler={createNamespaceModal} />;

class PullSecret extends SafetyFirst {
constructor (props) {
Expand Down Expand Up @@ -75,73 +85,45 @@ class PullSecret extends SafetyFirst {
}
}

const NamespaceSparklines = ({namespace}) => <div className="co-namespace-sparklines">
<h1 className="co-m-pane__title">Resource Usage</h1>
<div className="co-namespace-sparklines__wrapper">
<div className="row no-gutter">
<div className="col-md-6 col-xs-12">
<SparklineWidget heading="CPU Shares" query={`namespace:container_spec_cpu_shares:sum{namespace='${namespace.metadata.name}'} * 1000000`} limitQuery="sum(namespace:container_spec_cpu_shares:sum) * 1000000" limitText="cluster" units="numeric" />
</div>
<div className="col-md-6 col-xs-12">
<SparklineWidget heading="RAM" query={`namespace:container_memory_usage_bytes:sum{namespace='${namespace.metadata.name}'}`} limitQuery="sum(namespace:container_memory_usage_bytes:sum)" limitText="cluster" units="binaryBytes" />
</div>
</div>
</div>
</div>;

const Details = (namespace) => {
if (_.isEmpty(namespace)) {
const Details = (ns) => {
if (_.isEmpty(ns)) {
return <div className="empty-page">
<h1 className="empty-page__header">No namespace selected</h1>
<p className="empty-page__explanation">Namespaces organize and isolate your cluster resources from other things running on the cluster.</p>
</div>;
}

const deleteModal = {label: 'Delete Namespace', weight: 900};
if(namespace.metadata.name === 'default') {
deleteModal.label = <div className="dropdown__disabled" data-tip='Namespace "default" cannot be deleted'>{deleteModal.label}</div>;
ReactTooltip.rebuild();
} else {
deleteModal.callback = () => deleteNamespaceModal({resource: namespace});
}

const menuActions = [
Cog.factory.ModifyLabels(kindObj('namespace'), namespace),
Cog.factory.ModifyAnnotations(kindObj('namespace'), namespace),
deleteModal,
];

return <div className="details-page">
{namespace.status.phase !== 'Terminating' && <ActionsMenu actions={menuActions} />}
<h1 className="co-m-pane__title co-m-pane__body__top-controls">Namespace {namespace.metadata.name}</h1>
<dl>
<dt>Status</dt>
<dd>{namespace.status.phase}</dd>
<dt>Namespace Labels</dt>
<dd><LabelList kind="namespace" labels={namespace.metadata.labels} /></dd>
<dt>Annotations</dt>
<dd><a className="co-m-modal-link" onClick={Cog.factory.ModifyAnnotations(kindObj('namespace'), namespace).callback}>{pluralize(_.size(namespace.metadata.annotations), 'Annotation')}</a></dd>
<dt>Default Pull Secret</dt>
<dd><PullSecret namespace={namespace} /></dd>
</dl>
<NamespaceSparklines namespace={namespace} />
return <div>
<ResourceHeading resourceName="Namespace" />
<div className="co-m-pane__body">
<div className="row">
<div className="col-sm-6 col-xs-12">
<ResourceSummary resource={ns} showPodSelector={false} showNodeSelector={false} />
</div>
<div className="col-sm-6 col-xs-12">
<dl>
<dt>Status</dt>
<dd>{ns.status.phase}</dd>
<dt>Default Pull Secret</dt>
<dd><PullSecret namespace={ns} /></dd>
</dl>
</div>
</div>
</div>
<div className="co-m-pane__body">
<div className="row">
<h1 className="co-m-pane__title">Resource Usage</h1>
<div className="col-sm-6 col-xs-12 co-namespace-sparkline">
<SparklineWidget heading="CPU Shares" query={`namespace:container_spec_cpu_shares:sum{namespace='${ns.metadata.name}'} * 1000000`} limitQuery="sum(namespace:container_spec_cpu_shares:sum) * 1000000" limitText="cluster" units="numeric" />
</div>
<div className="col-sm-6 col-xs-12 co-namespace-sparkline">
<SparklineWidget heading="RAM" query={`namespace:container_memory_usage_bytes:sum{namespace='${ns.metadata.name}'}`} limitQuery="sum(namespace:container_memory_usage_bytes:sum)" limitText="cluster" units="binaryBytes" />
</div>
</div>
</div>
</div>;
};

const Header = () => <div className="co-m-facet-menu__title">Name</div>;
const List = makeList('Namespaces', 'namespace', Header, RowOfKind('namespace'));


const CreateButton = () => <button type="button" className="btn btn-primary co-m-pane__title__btn" onClick={() => createNamespaceModal()}>Create Namespace</button>;

export const NamespacesPage = () => <div>
<Helmet title="Namespaces" />
<NavTitle title="Namespaces" />
<TwoColumns list={List} topControls={CreateButton}>
<Details />
</TwoColumns>
</div>;

const NamespaceDropdown = connect(() => ({namespace: getActiveNamespace()}))(props => {
const {data, loaded, namespace, dispatch} = props;

Expand Down Expand Up @@ -178,3 +160,5 @@ export const NamespaceSelector = (props) => {
<NamespaceDropdown {...props} />
</Firehose>;
};

export const NamespacesDetailsPage = props => <DetailsPage {...props} pages={[navFactory.details(Details)]} menuActions={menuActions} />;
6 changes: 3 additions & 3 deletions frontend/public/components/node.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const NodeCLUpdateStatus = ({node}) => {

return <div>
<span><i className={updateStatus.className}></i>&nbsp;&nbsp;{updateStatus.text}</span>
{!_.isEmpty(newVersion) && !containerLinuxUpdateOperator.isSoftwareUpToDate(node) &&
{!_.isEmpty(newVersion) && !containerLinuxUpdateOperator.isSoftwareUpToDate(node) &&
<div>
<small className="">Container Linux {containerLinuxUpdateOperator.getVersion(node)} &#10141; {newVersion}</small>
</div>}
Expand Down Expand Up @@ -102,7 +102,7 @@ const NodeRow = ({obj: node, expand}) => {
<div className={isOperatorInstalled ? 'col-xs-2' : 'col-xs-4'}>
<NodeStatus node={node} />
</div>
{ isOperatorInstalled && <div className="col-xs-3">
{ isOperatorInstalled && <div className="col-xs-3">
<NodeCLStatusRow node={node} />
</div>}
<div className={isOperatorInstalled ? 'col-xs-3' : 'col-xs-4'}>
Expand Down Expand Up @@ -198,7 +198,7 @@ const Details = (node) => {
<dt>Provider ID</dt>
<dd>{cloudProviderNames([cloudProviderID(node)])}</dd>
{_.has(node, 'spec.unschedulable') && <dt>Unschedulable</dt>}
{_.has(node, 'spec.unschedulable') && <dd className="text-capitalize">{_.get(node, 'spec.unschedulable', '-').toString()}
{_.has(node, 'spec.unschedulable') && <dd className="text-capitalize">{_.get(node, 'spec.unschedulable', '-').toString()}
</dd>}
<dt>Created</dt>
<dd><Timestamp timestamp={node.metadata.creationTimestamp} /></dd>
Expand Down
1 change: 1 addition & 0 deletions frontend/public/components/resource-pages.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { DaemonSetsPage, DaemonSetsDetailsPage } from './daemonset';
export { DeploymentsPage, DeploymentsDetailsPage } from './deployment';
export { HorizontalPodAutoscalersPage, HorizontalPodAutoscalersDetailsPage } from './horizontal-pod-autoscaler';
export { JobsPage, JobsDetailsPage } from './job';
export { NamespacesPage, NamespacesDetailsPage } from './namespace';
export { NodesPage, NodesDetailsPage } from './node';
export { PodsPage, PodsDetailsPage } from './pod';
export { ReplicaSetsPage, ReplicaSetsDetailsPage } from './replicaset';
Expand Down
8 changes: 1 addition & 7 deletions frontend/public/components/utils/cog.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import ReactTooltip from 'react-tooltip';
import classNames from 'classnames';

import {k8s} from '../../module/k8s/k8s';
Expand All @@ -10,11 +9,6 @@ import { DropdownMixin, sortActions } from './dropdown';
import { history, kindObj } from './index';

export class Cog extends DropdownMixin {
componentDidMount () {
super.componentDidMount();
ReactTooltip.rebuild();
}

render () {
const onClick_ = (option, event) => {
event.preventDefault();
Expand Down Expand Up @@ -51,7 +45,7 @@ export class Cog extends DropdownMixin {

Cog.factory = {
Delete: (kind, obj) => ({
label: `Delete ${kind.label} ...`,
label: `Delete ${kind.label}...`,
weight: 900,
callback: () => confirmModal({
title: `Delete ${kind.label} `,
Expand Down
5 changes: 5 additions & 0 deletions frontend/public/components/utils/dropdown.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import classNames from 'classnames';
import ReactTooltip from 'react-tooltip';

import { history } from './index';

Expand Down Expand Up @@ -33,6 +34,10 @@ export class DropdownMixin extends React.PureComponent {
window.removeEventListener('click', this.listener);
}

componentWillUpdate () {
ReactTooltip.rebuild();
}

onClick_ (key, e) {
e.stopPropagation();

Expand Down
1 change: 0 additions & 1 deletion frontend/public/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ $fa-font-path: "./fonts";
@import "components/error";
@import "components/global-notification";
@import "components/list";
@import "components/namespace-sparklines";
@import "components/nav-title";
@import "components/node-ip-list";
@import "components/overflow";
Expand Down

0 comments on commit 1f774a0

Please sign in to comment.