Initial work on responsive/mobile design

This commit is contained in:
Eric Herbrandson
2019-04-11 18:26:47 -05:00
parent 4d82c4e3e6
commit 75009ba18e
46 changed files with 458 additions and 253 deletions
+16 -5
View File
@@ -1,10 +1,12 @@
import React, {Component, Fragment} from 'react';
import TitleBar from './components/titleBar';
import Menu from './components/menu';
import {Notifier} from './components/notifier';
import Error from './components/error';
import {registerHandler, initRouter} from './router';
import log from './utils/log';
import Button from './components/button';
import LogoSvg from './art/logoSvg';
import HamburgerSvg from './art/hamburgerSvg';
const genericError = (
<div id='content'>
@@ -24,19 +26,28 @@ class App extends Component {
}
render() {
const {content, hasError} = this.state || {};
const {content, hasError, menuToggled} = this.state || {};
return (
<>
<TitleBar />
<div id='titleBar'>
<Button className='button_clear titleBar_hamburger' onClick={() => this.setState({menuToggled: !menuToggled})}>
<HamburgerSvg className='svg_button' />
</Button>
<a href='#!'>
<LogoSvg className='titleBar_logo' />
</a>
</div>
<div id='shell'>
<Menu />
<Menu toggled={menuToggled} onClick={() => this.setState({menuToggled: false})} />
<Fragment key={Date.now()}>
{hasError ? genericError : content}
</Fragment>
<Notifier />
</div>
<Notifier />
</>
);
}
+9
View File
@@ -0,0 +1,9 @@
import React from 'react';
const HamburgerSvg = props => (
<svg version="1" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M4 10h24a2 2 0 0 0 0-4H4a2 2 0 0 0 0 4zm24 4H4a2 2 0 0 0 0 4h24a2 2 0 0 0 0-4zm0 8H4a2 2 0 0 0 0 4h24a2 2 0 0 0 0-4z"/>
</svg>
);
export default HamburgerSvg;
+41 -52
View File
@@ -5,63 +5,52 @@ import Field from './field';
const ContainersPanel = ({spec}) => (
<>
{spec && _.map(spec.containers, item => (
<div key={item.name} className='contentPanel'>
<Field name='Container'>{item.name}</Field>
<Field name='Image'>{item.image}</Field>
<>
<div className='contentPanel_header'>Container</div>
<div key={item.name} className='contentPanel'>
{item.args && (
<Field name='Args'>{item.args.join(' ')}</Field>
)}
<Field name='Container'>{item.name}</Field>
<Field name='Image'>{item.image}</Field>
{item.env && (
<Field name='Env'>
{item.env.map(x => (
<div key={x.name}>
{x.name}: {getVariableValue(x)}
</div>
))}
</Field>
)}
{item.args && (
<Field name='Args'>{item.args.join(' ')}</Field>
)}
{item.resources && item.resources.requests && (
<>
<Field name='Cpu Request'>{item.resources.requests.cpu}</Field>
<Field name='Memory Request'>{item.resources.requests.memory}</Field>
</>
)}
{item.env && (
<Field name='Env'>
{item.env.map(x => (
<div key={x.name}>
{x.name}: {getVariableValue(x)}
</div>
))}
</Field>
)}
{item.resources && item.resources.limits && (
<>
<Field name='Cpu Limit'>{item.resources.limits.cpu}</Field>
<Field name='Memory Limit'>{item.resources.limits.memory}</Field>
</>
)}
{item.resources && item.resources.requests && (
<>
<Field name='Cpu Request'>{item.resources.requests.cpu}</Field>
<Field name='Memory Request'>{item.resources.requests.memory}</Field>
</>
)}
{item.ports && (
<Field name='Ports'>
<table>
<thead>
<tr>
<th>Name</th>
<th>Container Port</th>
<th>Host Port</th>
<th>Protocol</th>
</tr>
</thead>
<tbody>
{item.ports.map((x, i) => (
<tr key={i}>
<td>{x.name}</td>
<td>{x.containerPort}</td>
<td>{x.hostPort}</td>
<td>{x.protocol}</td>
</tr>
))}
</tbody>
</table>
</Field>
)}
</div>
{item.resources && item.resources.limits && (
<>
<Field name='Cpu Limit'>{item.resources.limits.cpu}</Field>
<Field name='Memory Limit'>{item.resources.limits.memory}</Field>
</>
)}
{item.ports && (
<Field name='Ports'>
{item.ports.map((x, i) => (
<div key={i}>
{[x.name, x.containerPort, x.hostPort, x.protocol].filter(y => !!y).join(' • ')}
</div>
))}
</Field>
)}
</div>
</>
))}
</>
);
+5 -5
View File
@@ -8,10 +8,10 @@ import Sorter, {sortByDate} from './sorter';
export default function EventsPanel({items, filter, shortList, sort}) {
return (
<div className='contentPanel'>
<table>
<table className='wrapped'>
<thead>
<tr>
<th className='th_icon'>
<th className='th_icon optional_medium'>
<Sorter field='involvedObject.kind' sort={sort}>Type</Sorter>
</th>
<th>
@@ -22,7 +22,7 @@ export default function EventsPanel({items, filter, shortList, sort}) {
<Sorter field={sortByName} sort={sort}>Name</Sorter>
</th>
)}
<th>
<th className='optional_small'>
<Sorter field='reason' sort={sort}>Reason</Sorter>
</th>
<th>
@@ -38,7 +38,7 @@ export default function EventsPanel({items, filter, shortList, sort}) {
sort={sort}
row={x => (
<tr key={x.metadata.name}>
<td className='td_icon'>
<td className='td_icon optional_medium'>
<ResourceSvg
resource={x.involvedObject.kind}
className={getTypeClass(x.type)}
@@ -49,7 +49,7 @@ export default function EventsPanel({items, filter, shortList, sort}) {
{!shortList && (
<td>{x.involvedObject.namespace}:{x.involvedObject.name}</td>
)}
<td>{x.reason}</td>
<td className='optional_small'>{x.reason}</td>
<td>{x.message}</td>
</tr>
)}
+18
View File
@@ -1,3 +1,4 @@
@import '../scss/settings.scss';
.field_set {
margin: 20px 0;
@@ -15,3 +16,20 @@
margin-left: 20px;
word-break: break-all;
}
@media only screen and (max-width: $media-small) {
.field_set {
flex-direction: column;
align-items: flex-start;
}
.field_name {
text-align: left;
flex: 0;
}
.field_value {
margin-left: 0;
}
}
+3 -3
View File
@@ -5,7 +5,7 @@ import Cancel from '../art/cancelSvg';
const InputFilter = ({filter, onChange}) => (
<>
<input
className='header_filter header_fill'
className='header_filter header_fill optional_xsmall'
type='text'
placeholder='type to filter'
value={filter}
@@ -17,9 +17,9 @@ const InputFilter = ({filter, onChange}) => (
/>
{filter.length === 0 ? (
<Search />
<Search className='optional_xsmall' />
) : (
<Cancel className='svg_button svg_error' onClick={() => onChange('')} />
<Cancel className='svg_button svg_error optional_xsmall' onClick={() => onChange('')} />
)}
</>
);
+6 -6
View File
@@ -31,18 +31,18 @@ export function TableBody({items, filter, colSpan, sort, row}) {
export function MetadataHeaders({includeNamespace, sort}) {
return (
<>
<th className='th_icon'>
<th className='th_icon optional_small'>
<Sorter field='kind' sort={sort}>Type</Sorter>
</th>
<th>
<Sorter field='metadata.name' sort={sort}>Name</Sorter>
</th>
{includeNamespace && (
<th>
<th className='optional_medium'>
<Sorter field='metadata.namespace' sort={sort}>Namespace</Sorter>
</th>
)}
<th>
<th className='optional_medium'>
<Sorter field={sortByDate} sort={sort}>Age</Sorter>
</th>
</>
@@ -52,7 +52,7 @@ export function MetadataHeaders({includeNamespace, sort}) {
export function MetadataColumns({item, href, includeNamespace, isError}) {
return (
<>
<td className='td_icon'>
<td className='td_icon optional_small'>
<ResourceSvg
resource={item.kind}
className={isError ? 'svg_error' : undefined}
@@ -64,12 +64,12 @@ export function MetadataColumns({item, href, includeNamespace, isError}) {
</td>
{includeNamespace && (
<td>
<td className='optional_medium'>
{item.metadata.namespace || 'All Namespaces'}
</td>
)}
<td>
<td className='optional_medium'>
{moment(item.metadata.creationTimestamp).fromNow(true)}
</td>
</>
+22 -21
View File
@@ -9,50 +9,51 @@ import AddSvg from '../art/addSvg';
export default class Menu extends Base {
render() {
const {onClick, toggled} = this.props;
const {showAdd} = this.state || {};
return (
<div id='menu'>
<div id='menu' className={toggled ? 'menu_toggled' : undefined}>
{/* Cluster */}
<Group>
<MenuItem title='Cluster' path='' resource='Logo' />
<MenuItem title='Nodes' path='node' resource='Node' />
<MenuItem title='Namespaces' path='namespace' resource='Namespace' />
<MenuItem title='Cluster' path='' resource='Logo' onClick={onClick} />
<MenuItem title='Nodes' path='node' resource='Node' onClick={onClick} />
<MenuItem title='Namespaces' path='namespace' resource='Namespace' onClick={onClick} />
</Group>
{/* Workloads */}
<Group>
<MenuItem title='Workloads' path='workload' resource='Deployment' />
<MenuItem title='Services' path='service' resource='Service' />
<MenuItem title='Replicas' path='replicaset' resource='ReplicaSet' />
<MenuItem title='Pods' path='pod' resource='Pod' />
<MenuItem title='Ingresses' path='ingress' resource='Ingress' />
<MenuItem title='Config' path='configmap' resource='ConfigMap' />
<MenuItem title='Workloads' path='workload' resource='Deployment' onClick={onClick} />
<MenuItem title='Services' path='service' resource='Service' onClick={onClick} />
<MenuItem title='Replicas' path='replicaset' resource='ReplicaSet' onClick={onClick} />
<MenuItem title='Pods' path='pod' resource='Pod' onClick={onClick} />
<MenuItem title='Ingresses' path='ingress' resource='Ingress' onClick={onClick} />
<MenuItem title='Config' path='configmap' resource='ConfigMap' onClick={onClick} />
</Group>
{/* Storage */}
<Group>
<MenuItem title='Storage' path='storageclass' resource='StorageClass' />
<MenuItem title='Volumes' path='persistentvolume' resource='PersistentVolume' />
<MenuItem title='Claims' path='persistentvolumeclaim' resource='PersistentVolumeClaim' />
<MenuItem title='Storage' path='storageclass' resource='StorageClass' onClick={onClick} />
<MenuItem title='Volumes' path='persistentvolume' resource='PersistentVolume' onClick={onClick} />
<MenuItem title='Claims' path='persistentvolumeclaim' resource='PersistentVolumeClaim' onClick={onClick} />
</Group>
{/* Security */}
<Group>
<MenuItem title='Accounts' path='serviceaccount' resource='ServiceAccount' />
<MenuItem title='Roles' path='role' resource='Role' additionalPaths={['clusterrole']} />
<MenuItem title='Bindings' path='rolebinding' resource='Role' additionalPaths={['clusterrolebinding']} />
<MenuItem title='Secrets' path='secret' resource='Secret' />
<MenuItem title='Accounts' path='serviceaccount' resource='ServiceAccount' onClick={onClick} />
<MenuItem title='Roles' path='role' resource='Role' additionalPaths={['clusterrole']} onClick={onClick} />
<MenuItem title='Bindings' path='rolebinding' resource='Role' additionalPaths={['clusterrolebinding']} onClick={onClick} />
<MenuItem title='Secrets' path='secret' resource='Secret' onClick={onClick} />
</Group>
<Group>
<MenuItem title='Profile' path='account' resource='User' />
<MenuItem title='Profile' path='account' resource='User' onClick={onClick} />
</Group>
<Group>
<div className='menu_itemApply'>
<button className='menu_item button_clear' onClick={() => this.setState({showAdd: true})}>
<button className='menu_item button_clear' onClick={() => { this.setState({showAdd: true}); onClick(); }}>
<AddSvg className='menu_icon' />
<div className='menu_title'>Apply</div>
</button>
@@ -75,10 +76,10 @@ function MenuItem(item) {
const currentPath = getRootPath();
const paths = getPaths(item);
const className = paths.includes(currentPath) ? 'menu_item menu_itemSelected' : 'menu_item';
const {path, title, resource} = item;
const {path, title, resource, onClick} = item;
return (
<a href={`#!${path}`} title={title} className={className}>
<a href={`#!${path}`} title={title} className={className} onClick={onClick}>
<ResourceSvg className='menu_icon' resource={resource} />
<span className='menu_title'>{title}</span>
</a>
+43 -20
View File
@@ -19,26 +19,6 @@
transition: transform 0.33s;
}
.menu_subMenu a:nth-child(1) {
transition-delay: 0.1s;
}
.menu_subMenu a:nth-child(2) {
transition-delay: 0.2s;
}
.menu_subMenu a:nth-child(3) {
transition-delay: 0.3s;
}
.menu_subMenu a:nth-child(4) {
transition-delay: 0.4s;
}
.menu_subMenu a:nth-child(5) {
transition-delay: 0.5s;
}
.menu_subMenuSelected {
max-height: 1000px;
}
@@ -78,3 +58,46 @@ a.menu_itemSelected, a.menu_itemSelected:visited {
display: flex;
justify-content: center;
}
.menu_subMenu a:nth-child(1) {
transition-delay: 0.1s;
}
.menu_subMenu a:nth-child(2) {
transition-delay: 0.2s;
}
.menu_subMenu a:nth-child(3) {
transition-delay: 0.3s;
}
.menu_subMenu a:nth-child(4) {
transition-delay: 0.4s;
}
.menu_subMenu a:nth-child(5) {
transition-delay: 0.5s;
}
@media only screen and (max-width: $media-small) and (min-width: $media-xsmall) {
button.menu_item, a.menu_item, a.menu_item:visited {
svg {
width: 25px;
height: 25px;
}
}
.menu_title {
display: none;
}
}
@media only screen and (max-width: $media-xsmall) {
.menu_subMenu {
max-height: initial;
}
.menu_subMenu a {
transform: initial;
}
}
+4 -4
View File
@@ -37,8 +37,8 @@ export default class NodesPanel extends Base {
<thead>
<tr>
<MetadataHeaders sort={sort} />
<th>Labels</th>
<th><Sorter field={getReadyStatus} sort={sort}>Ready</Sorter></th>
<th className='optional_medium'>Labels</th>
<th className='optional_small'><Sorter field={getReadyStatus} sort={sort}>Ready</Sorter></th>
<th><Sorter field={this.sortByCpu} sort={sort}>Cpu</Sorter></th>
<th><Sorter field={this.sortByRam} sort={sort}>Ram</Sorter></th>
</tr>
@@ -47,8 +47,8 @@ export default class NodesPanel extends Base {
<TableBody items={items} filter={filter} sort={sort} colSpan='8' row={x => (
<tr key={x.metadata.uid}>
<MetadataColumns item={x} href={`#!node/${x.metadata.name}`} />
<td>{objectMap(x.metadata.labels)}</td>
<td>{getReadyStatus(x)}</td>
<td className='optional_medium'>{objectMap(x.metadata.labels)}</td>
<td className='optional_small'>{getReadyStatus(x)}</td>
<td><CpuPercent item={x} metrics={metrics} /></td>
<td><RamPercent item={x} metrics={metrics} /></td>
</tr>
+4 -12
View File
@@ -36,11 +36,9 @@ export default class PodsPanel extends Base {
<tr>
<MetadataHeaders sort={sort} includeNamespace={!skipNamespace} />
{!skipNodeName && (
<th><Sorter field='spec.nodeName' sort={sort}>Node</Sorter></th>
<th className='optional_small'><Sorter field='spec.nodeName' sort={sort}>Node</Sorter></th>
)}
<th><Sorter field='status.phase' sort={sort}>Status</Sorter></th>
<th><Sorter field={getContainerCount} sort={sort}>Containers</Sorter></th>
<th><Sorter field={getRestartCount} sort={sort}>Restarts</Sorter></th>
<th className='optional_medium'><Sorter field={getRestartCount} sort={sort}>Restarts</Sorter></th>
<th>
<Sorter field={this.sortByCpu} sort={sort}>
Cpu
@@ -68,10 +66,8 @@ export default class PodsPanel extends Base {
includeNamespace={!skipNamespace}
href={`#!pod/${x.metadata.namespace}/${x.metadata.name}`}
/>
{!skipNodeName && <td>{x.spec.nodeName}</td>}
<td>{x.status.phase}</td>
<td>{getContainerCount(x)}</td>
<td>{getRestartCount(x)}</td>
{!skipNodeName && <td className='optional_small'>{x.spec.nodeName}</td>}
<td className='optional_medium'>{getRestartCount(x)}</td>
<td><Cpu item={x} metrics={metrics} /></td>
<td><Ram item={x} metrics={metrics} /></td>
</tr>
@@ -82,10 +78,6 @@ export default class PodsPanel extends Base {
}
}
function getContainerCount({spec}) {
return spec.containers ? spec.containers.length : 0;
}
function getRestartCount({status}) {
return _.sumBy(status.containerStatuses, 'restartCount');
}
+3 -3
View File
@@ -19,12 +19,12 @@ export default class ReplicaSetsPanel extends Base {
<thead>
<tr>
<MetadataHeaders sort={sort} includeNamespace={includeNamespace} />
<th>
<th className='optional_small'>
<Sorter field='status.observedGeneration' sort={sort}>Generations</Sorter>
</th>
<th className='replicaSetsPanel_replicas'>
<Sorter field='spec.replicas' sort={sort}>Replicas</Sorter>
<label className='replicaSetsPanel_switch'>
<label className='replicaSetsPanel_switch optional_xsmall'>
<Switch
checked={activeOnly}
onChange={x => this.setState({activeOnly: x})}
@@ -51,7 +51,7 @@ export default class ReplicaSetsPanel extends Base {
includeNamespace={includeNamespace}
href={`#!replicaset/${x.metadata.namespace}/${x.metadata.name}`}
/>
<td>{x.status.observedGeneration}</td>
<td className='optional_small'>{x.status.observedGeneration}</td>
<td>{x.spec.replicas} / {x.status.replicas}</td>
</tr>
)}
+5 -5
View File
@@ -18,12 +18,12 @@ export default class ScaleButton extends Base {
{scaleInfo && (
<Modal isOpen={true} className='modal_modal' overlayClassName='modal_overlay' onRequestClose={() => this.close()}>
<div>
<div className='scaleButton_label'>Desired count</div>
<div className='scaleButton'>
<div className='scaleButton_label'>Desired Count</div>
<input
type='number'
className='scaleButton_input'
min="0"
min='0'
defaultValue={scaleInfo.spec.replicas || 0}
onChange={x => this.setState({value: parseInt(x.target.value, 10)})}
/>
@@ -57,8 +57,8 @@ export default class ScaleButton extends Base {
}
close() {
// Use setTimeout to prevent the following React warning:
// "Warning: Can't perform a React state update on an unmounted component."
// Use setTimeout to prevent the following React warning:
// "Warning: Can't perform a React state update on an unmounted component."
setTimeout(() => this.setState({scaleInfo: null}), 0);
}
}
+5
View File
@@ -1,5 +1,10 @@
@import '../scss/settings.scss';
.scaleButton {
display: flex;
flex-direction: column;
}
.scaleButton_label {
color: $color-light;
}
-14
View File
@@ -1,14 +0,0 @@
import './titleBar.scss';
import React from 'react';
import LogoSvg from '../art/logoSvg';
const TitleBar = () => (
<div id='titleBar'>
<a href='#!'>
<LogoSvg className='titleBar_logo' />
</a>
</div>
);
export default TitleBar;
-17
View File
@@ -1,17 +0,0 @@
@import '../scss/settings.scss';
#titleBar {
height: 55px;
background-color: $color-accent;
color: $color-white;
display: flex;
align-items: center;
padding: 0 8px;
font-size: $font-size-large;
justify-content: space-between;
z-index: 1;
}
.titleBar_addButton {
fill: $color-lightest;
}
+18
View File
@@ -52,7 +52,15 @@ $ct-series-colors: (
.chart_donutLabel {
grid-column: 1;
grid-row: 1;
font-size: $font-size-large;
}
.charts_itemLabel {
font-size: $font-size-medium;
color: $color-light;
margin-top: 8px;
text-align: right;
text-transform: uppercase;
}
.chart_innerLabel {
@@ -64,3 +72,13 @@ $ct-series-colors: (
grid-column: 1;
grid-row: 1;
}
@media only screen and (max-width: $media-small) {
.chart_donutLabel {
font-size: $font-size;
}
.charts_itemLabel {
font-size: $font-size;
}
}
+26 -1
View File
@@ -3,10 +3,14 @@
.contentPanel {
box-shadow: 0 0 5px 0 $color-light;
margin: 35px 0;
padding: 20px;
padding: 0 20px 10px 20px;
overflow: scroll;
}
.contentPanel_header {
display: none;
}
.contentPanel_label {
margin-right: 5px;
}
@@ -15,3 +19,24 @@
color: $color-error;
font-weight: bold;
}
@media only screen and (max-width: $media-xsmall) {
.contentPanel {
box-shadow: none;
padding: 0;
margin-top: 40px;
}
.contentPanel_header {
display: block;
position: relative;
top: 25px;
padding: 5px;
font-size: $font-size-large;
text-align: right;
text-transform: uppercase;
color: $color-dark;
background-color: $color-lightest;
}
}
+8 -1
View File
@@ -8,7 +8,7 @@
}
#header > .button_headerAction {
transform: translateY(-50px);
transform: translateY(-80px);
transition-duration: $animation-length;
transition-property: transform;
}
@@ -89,3 +89,10 @@ input[type="text"].header_filter {
#header .button_headerAction:nth-child(9) {
transition-delay: 0.9s;
}
@media only screen and (max-width: $media-xsmall) {
.header_label {
font-size: $font-size-medium;
}
}
+25 -9
View File
@@ -1,4 +1,13 @@
.modal_overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.33);
}
.modal_modal {
position: absolute;
top: 50%;
@@ -12,17 +21,24 @@
transform: translate(-50%, -50%);
}
.modal_overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.33);
}
.modal_actions {
display: flex;
justify-content: flex-end;
align-items: center;
}
@media only screen and (max-width: $media-xsmall) {
.modal_overlay {
z-index: 300;
}
.modal_modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
transform: initial;
}
}
+21 -1
View File
@@ -9,7 +9,7 @@ thead > tr > th {
font-weight: normal;
text-transform: uppercase;
text-align: left;
padding: 1rem;
padding: 0.75rem;
border-bottom: 1px solid $color-light;
}
@@ -30,3 +30,23 @@ td {
margin-top: -2px;
text-transform: uppercase;
}
@media only screen and (max-width: $media-xsmall) {
thead > tr > th {
padding: 0.75rem 0.25rem;
}
td {
padding: 0.75rem 0.25rem;
}
.wrapped > thead {
display: none;
}
.wrapped > tbody > tr {
display: flex;
flex-direction: column;
margin-bottom: 35px;
}
}
+79 -5
View File
@@ -27,6 +27,7 @@ html {
font-size: $font-size;
font-weight: 300;
color: $color-font;
min-width: 320px;
}
#root {
@@ -34,11 +35,6 @@ html {
flex-direction: column;
}
.titleBar_logo {
width: 80px;
height: 80px;
}
#shell {
display: flex;
min-height: calc(100vh - 55px);
@@ -55,3 +51,81 @@ html {
flex: 1;
width: calc(100vw - 72px);
}
#titleBar {
height: 55px;
background-color: $color-accent;
color: $color-white;
display: flex;
align-items: center;
padding: 0 8px;
font-size: $font-size-large;
justify-content: space-between;
z-index: 1;
}
.titleBar_logo {
width: 80px;
height: 80px;
}
button.titleBar_hamburger {
display: none;
}
.titleBar_hamburger > svg {
fill: $color-white;
}
@media only screen and (max-width: $media-medium) {
.optional_medium {
display: none;
}
}
@media only screen and (max-width: $media-small) {
.optional_small {
display: none;
}
#menu {
width: 44px
}
#content {
width: calc(100vw - 44px);
}
}
@media only screen and (max-width: $media-xsmall) {
#content {
padding: 0 15px;
width: 100vw;
}
.optional_xsmall {
display: none;
}
#menu {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 200;
width: 72px;
overflow-y: scroll;
box-shadow: 0 0 5px 0 $color-light;
transform: translateX(-80px);
transition: transform 0.33s;
}
#menu.menu_toggled {
transform: translateX(0);
}
button.titleBar_hamburger {
display: initial;
}
}
+5 -1
View File
@@ -15,6 +15,10 @@ $animation-length: .25s;
$font: sans-serif;
$font-size-small: 6pt;
$font-size: 10pt;
$font-size-medium: 16pt;
$font-size-medium: 13pt;
$font-size-large: 16pt;
$font-size-xlarge: 100px;
$media-xsmall: 606px;
$media-small: 812px;
$media-medium: 1024px;
+1 -1
View File
@@ -44,7 +44,7 @@ export default class Auth extends Base {
<div className='auth'>
{!useTokenLogin ? <Loading /> : (
<>
<LogoSvg />
<LogoSvg className='optional_small' />
<input
type='password'
className='auth_input'
+13 -3
View File
@@ -14,7 +14,7 @@
}
.auth_input {
font-size: $font-size-medium;
font-size: $font-size-large;
padding: 0.5rem;
width: 400px;
margin: 0 5px;
@@ -22,5 +22,15 @@
.auth_button {
border: solid 1px $color-accent;
font-size: $font-size-medium;
}
font-size: $font-size-large;
}
@media only screen and (max-width: $media-small) {
.auth {
flex-direction: column;
}
.auth_input {
width: 300px;
}
}
+1
View File
@@ -48,6 +48,7 @@ export default class ClusterRole extends Base {
)}
</div>
<div className='contentPanel_header'>Rules</div>
<div className='contentPanel'>
<table>
<thead>
+7 -6
View File
@@ -61,20 +61,21 @@ export default class ClusterRoleBinding extends Base {
)}
</div>
<div className='contentPanel_header'>Subjects</div>
<div className='contentPanel'>
<table>
<thead>
<tr>
<th className='th_icon'>
<th className='th_icon optional_small'>
<Sorter sort={sort} field='kind'>Type</Sorter>
</th>
<th>
<Sorter sort={sort} field='name'>Name</Sorter>
</th>
<th>
<th className='optional_small'>
<Sorter sort={sort} field='namespace'>Namespace</Sorter>
</th>
<th>
<th className='optional_small'>
<Sorter sort={sort} field='apiGroup'>Api Group</Sorter>
</th>
</tr>
@@ -82,15 +83,15 @@ export default class ClusterRoleBinding extends Base {
<TableBody items={subjects} colSpan='4' sort={sort} row={x => (
<tr key={x.name}>
<td>
<td className='td_icon optional_small'>
<ResourceSvg resource={x.kind} />
<div className='td_iconLabel'>{x.kind}</div>
</td>
<td>
{x.kind === 'ServiceAccount' ? (<a href={getSubjectHref(x)}>{x.name}</a>) : x.name}
</td>
<td>{x.namespace}</td>
<td>{x.apiGroup}</td>
<td className='optional_small'>{x.namespace}</td>
<td className='optional_small'>{x.apiGroup}</td>
</tr>
)} />
</table>
+2
View File
@@ -82,6 +82,7 @@ export default class CronJob extends Base {
{/* TODO: this actually need to be a list of jobs */}
<div className='contentPanel_header'>Pods</div>
<PodsPanel
items={filteredPods}
sort={podsSort}
@@ -89,6 +90,7 @@ export default class CronJob extends Base {
skipNamespace={true}
/>
<div className='contentPanel_header'>Events</div>
<EventsPanel
sort={eventsSort}
shortList={true}
+4
View File
@@ -85,7 +85,11 @@ export default class DaemonSet extends Base {
</div>
<ContainersPanel spec={item && item.spec.template.spec} />
<div className='contentPanel_header'>Pods</div>
<PodsPanel items={filteredPods} sort={podsSort} metrics={filteredMetrics} skipNamespace={true} />
<div className='contentPanel_header'>Events</div>
<EventsPanel sort={eventsSort} shortList={true} items={filteredEvents} />
</div>
);
+3
View File
@@ -94,12 +94,14 @@ export default class Deployment extends Base {
<ContainersPanel spec={item && item.spec.template.spec} />
<div className='contentPanel_header'>Replica Sets</div>
<ReplicaSetsPanel
items={filteredReplicaSets}
sort={replicaSetsSort}
includeNamespace={false}
/>
<div className='contentPanel_header'>Pods</div>
<PodsPanel
items={filteredPods}
sort={podsSort}
@@ -107,6 +109,7 @@ export default class Deployment extends Base {
skipNamespace={true}
/>
<div className='contentPanel_header'>Events</div>
<EventsPanel
shortList={true}
sort={eventsSort}
+10 -1
View File
@@ -31,4 +31,13 @@
.editorModal_spacer {
flex: 1;
}
}
@media only screen and (max-width: $media-xsmall) {
.editorModal {
width: initial;
max-width: initial;
max-height: initial;
height: calc(100vh - 40px);
}
}
+1
View File
@@ -47,6 +47,7 @@ export default class Ingress extends Base {
)}
</div>
<div className='contentPanel_header'>Rules</div>
<div className='contentPanel'>
<table>
<thead>
+2
View File
@@ -85,6 +85,7 @@ export default class Job extends Base {
<ContainersPanel spec={item && item.spec.template.spec} />
<div className='contentPanel_header'>Pods</div>
<PodsPanel
items={filteredPods}
sort={podsSort}
@@ -92,6 +93,7 @@ export default class Job extends Base {
skipNamespace={true}
/>
<div className='contentPanel_header'>Events</div>
<EventsPanel
shortList={true}
sort={eventsSort}
+1
View File
@@ -43,6 +43,7 @@ export default class Namespace extends Base {
)}
</div>
<div className='contentPanel_header'>Events</div>
<EventsPanel items={events} />
</div>
);
+7 -7
View File
@@ -71,9 +71,9 @@ export default class Node extends Base {
<tr>
<th>Condition</th>
<th>Status</th>
<th>Transition</th>
<th>Reason</th>
<th>Message</th>
<th className='optional_medium'>Transition</th>
<th className='optional_small'>Reason</th>
<th className='optional_small'>Message</th>
</tr>
</thead>
<tbody>
@@ -81,9 +81,9 @@ export default class Node extends Base {
<tr key={x.type}>
<td>{x.type}</td>
<td>{x.status}</td>
<td>{moment(x.lastTransitionTime).fromNow()}</td>
<td>{x.reason}</td>
<td>{x.message}</td>
<td className='optional_medium'>{moment(x.lastTransitionTime).fromNow()}</td>
<td className='optional_small'>{x.reason}</td>
<td className='optional_small'>{x.message}</td>
</tr>
))}
</tbody>
@@ -91,7 +91,7 @@ export default class Node extends Base {
)}
</div>
<div className='contentPanel_header'>Pods</div>
<PodsPanel
items={filteredPods}
sort={podsSort}
+3 -3
View File
@@ -68,7 +68,7 @@ function CpuTotalsChart({items, metrics}) {
) : (
<LoadingChart />
)}
<div className='charts_itemLabel'>Cpu Cores Used</div>
<div className='charts_itemLabel'>Cores</div>
</div>
);
}
@@ -78,11 +78,11 @@ function RamTotalsChart({items, metrics}) {
return (
<div className='charts_item'>
{totals ? (
<Chart used={totals.used} available={totals.available} />
<Chart used={totals.used} usedSuffix='Gb' available={totals.available} availableSuffix='Gb' />
) : (
<LoadingChart />
)}
<div className='charts_itemLabel'>GB Ram Used</div>
<div className='charts_itemLabel'>Ram</div>
</div>
);
}
+10 -10
View File
@@ -39,18 +39,18 @@ export default class PersistentVolumeClaims extends Base {
<thead>
<tr>
<MetadataHeaders includeNamespace={true} sort={sort} />
<th>
<th className='optional_small'>
<Sorter field='status.phase' sort={sort}>Status</Sorter>
</th>
<th>
<th className='optional_medium'>
<Sorter field='spec.storageClassName' sort={sort}>Class Name</Sorter>
</th>
<th>
<Sorter field={getAccessModes} sort={sort}>Access Modes</Sorter>
</th>
<th>
<th className='optional_medium'>
<Sorter field='spec.volumeName' sort={sort}>Volume</Sorter>
</th>
<th className='optional_medium'>
<Sorter field={getAccessModes} sort={sort}>Access Modes</Sorter>
</th>
<th>
<Sorter field={getDiskSpace} sort={sort}>Capacity</Sorter>
</th>
@@ -64,10 +64,10 @@ export default class PersistentVolumeClaims extends Base {
includeNamespace={true}
href={`#!persistentvolumeclaim/${x.metadata.namespace}/${x.metadata.name}`}
/>
<td>{x.status.phase}</td>
<td>{x.spec.storageClassName}</td>
<td>{getAccessModes(x, ' • ')}</td>
<td>{x.spec.volumeName}</td>
<td className='optional_small'>{x.status.phase}</td>
<td className='optional_medium'>{x.spec.storageClassName}</td>
<td className='optional_medium'>{x.spec.volumeName}</td>
<td className='optional_medium'>{getAccessModes(x, ' • ')}</td>
<td>{x.status.capacity && x.status.capacity.storage}</td>
</tr>
)} />
+2 -2
View File
@@ -36,7 +36,7 @@ export default class PersistentVolumes extends Base {
<thead>
<tr>
<MetadataHeaders sort={sort} />
<th>
<th className='optional_small'>
<Sorter field='status.phase' sort={sort}>Status</Sorter>
</th>
<th>
@@ -51,7 +51,7 @@ export default class PersistentVolumes extends Base {
item={x}
href={`#!persistentvolume/${x.metadata.name}`}
/>
<td>{x.status.phase}</td>
<td className='optional_small'>{x.status.phase}</td>
<td>{x.spec.capacity && x.spec.capacity.storage}</td>
</tr>
)} />
+2
View File
@@ -117,6 +117,8 @@ export default class Pod extends Base {
</div>
<ContainersPanel spec={item && item.spec} />
<div className='contentPanel_header'>Events</div>
<EventsPanel items={filteredEvents} />
</div>
);
+2
View File
@@ -93,6 +93,7 @@ export default class ReplicaSet extends Base {
<ContainersPanel spec={item && item.spec.template.spec} />
<div className='contentPanel_header'>Pods</div>
<PodsPanel
items={filteredPods}
sort={podsSort}
@@ -100,6 +101,7 @@ export default class ReplicaSet extends Base {
skipNamespace={true}
/>
<div className='contentPanel_header'>Events</div>
<EventsPanel
shortList={true}
sort={eventsSort}
+1
View File
@@ -48,6 +48,7 @@ export default class Role extends Base {
)}
</div>
<div className='contentPanel_header'>Rules</div>
<div className='contentPanel'>
<table>
<thead>
+9 -6
View File
@@ -59,28 +59,31 @@ export default class RoleBinding extends Base {
)}
</div>
<div className='contentPanel_header'>Subjects</div>
<div className='contentPanel'>
<table>
<thead>
<tr>
<th className='th_icon'><Sorter filed='kind' sort={sort}>Type</Sorter></th>
<th className='th_icon optional_small'>
<Sorter filed='kind' sort={sort}>Type</Sorter>
</th>
<th><Sorter field='name' sort={sort}>Name</Sorter></th>
<th><Sorter field='namespace' sort={sort}>Namespace</Sorter></th>
<th><Sorter field='apiGroup' sort={sort}>Api Group</Sorter></th>
<th className='optional_small'><Sorter field='namespace' sort={sort}>Namespace</Sorter></th>
<th className='optional_small'><Sorter field='apiGroup' sort={sort}>Api Group</Sorter></th>
</tr>
</thead>
<TableBody items={subjects} sort={sort} colSpan='4' row={x => (
<tr key={x.name}>
<td>
<td className='td_icon optional_small'>
<ResourceSvg resource={x.kind} />
<div className='td_iconLabel'>{x.kind}</div>
</td>
<td>
{x.kind === 'ServiceAccount' ? (<a href={getSubjectHref(x)}>{x.name}</a>) : x.name}
</td>
<td>{x.namespace}</td>
<td>{x.apiGroup}</td>
<td className='optional_small'>{x.namespace}</td>
<td className='optional_small'>{x.apiGroup}</td>
</tr>
)} />
</table>
+7 -21
View File
@@ -1,7 +1,7 @@
import _ from 'lodash';
import React from 'react';
import api from '../services/api';
import EventsPanel from '../components/eventsPanel';
import {TableBody} from '../components/listViewHelpers';
import Base from '../components/base';
import DeleteButton from '../components/deleteButton';
import Field from '../components/field';
@@ -26,7 +26,6 @@ export default class Service extends Base {
render() {
const {namespace, name} = this.props;
const {item, events} = this.state || {};
const ports = item && item.spec.ports;
const filteredEvents = filterByOwner(events, item);
@@ -54,30 +53,17 @@ export default class Service extends Base {
<Field name='Affinity' value={item.spec.sessionAffinity} />
<Field name='Selector' value={item.spec.selector && item.spec.selector.app} />
<Field name='Ports'>
<table>
<thead>
<tr>
<th>Name</th>
<th>Port</th>
<th>Protocol</th>
<th>Target</th>
</tr>
</thead>
<TableBody items={ports} colSpan='4' row={x => (
<tr key={x.port}>
<td>{x.name}</td>
<td>{x.port}</td>
<td>{x.protocol}</td>
<td>{x.targetPort}</td>
</tr>
)} />
</table>
{_.map(item.spec.ports, x => (
<div key={x.port}>
{[x.name, x.port, x.targetPort, x.protocol].filter(y => !!y).join(' • ')}
</div>
))}
</Field>
</div>
)}
</div>
<div className='contentPanel_header'>Events</div>
<EventsPanel shortList={true} items={filteredEvents} />
</div>
);
+2
View File
@@ -81,6 +81,7 @@ export default class StatefulSet extends Base {
<ContainersPanel spec={item && item.spec.template.spec} />
<div className='contentPanel_header'>Pods</div>
<PodsPanel
items={filteredPods}
sort={podsSort}
@@ -88,6 +89,7 @@ export default class StatefulSet extends Base {
skipNamespace={true}
/>
<div className='contentPanel_header'>Events</div>
<EventsPanel
shortList={true}
sort={eventsSort}
+2 -2
View File
@@ -36,7 +36,7 @@ export default class StorageClasses extends Base {
<tr>
<MetadataHeaders sort={sort} />
<th><Sorter field='reclaimPolicy' sort={sort}>Reclaim Policy</Sorter></th>
<th><Sorter field='volumeBindingMode' sort={sort}>Mode</Sorter></th>
<th className='optional_small'><Sorter field='volumeBindingMode' sort={sort}>Mode</Sorter></th>
</tr>
</thead>
@@ -47,7 +47,7 @@ export default class StorageClasses extends Base {
href={`#!storageclass/${x.metadata.name}`}
/>
<td>{x.reclaimPolicy}</td>
<td>{x.volumeBindingMode}</td>
<td className='optional_small'>{x.volumeBindingMode}</td>
</tr>
)} />
</table>
-6
View File
@@ -115,12 +115,6 @@ async function logClusterInfo() {
const apis = apisResponse.body.groups.map(x => x.preferredVersion.groupVersion).sort();
const apisJson = JSON.stringify(apis, null, 4);
console.log('Available APIs: ', apisJson);
const authClient = kc.makeApiClient(k8s.Authorization_v1Api);
const spec = {resourceAttributes: {}};
const authResponse = await authClient.createSelfSubjectAccessReview({spec});
const authJson = JSON.stringify(authResponse.body, null, 4);
console.log('Auth Response: ', authJson);
} catch (err) {
console.error('Error getting cluster info', err);
}