mirror of https://github.com/linkerd/linkerd2.git
Move the dashboard's component library from antd to material-ui (#1776)
Switch the dashboard's component library from antd to material-ui. There are extensive changes to most of the frontend components in the app. This branch changes all uses of antd components to their closest equivalent in material. There is still a lot of polish that needs to go into the look of individual components, but since the major component rewrites are done, I think get this work in so that further work can be done in smaller branches. Changes in this branch: - add Material-UI 3.2.2 to the project - replace all uses of antd with material-ui components - remove antd from the project - slight modifications of eslint rules - restructuring of app components to be rendered under the Navigation component - deleted most of our css (replaced with material's inline styles) - pinned package versions in package.json (mostly removing ^)
This commit is contained in:
parent
f549868033
commit
e69da1b8a8
|
@ -1,8 +1,5 @@
|
|||
{
|
||||
"presets":[
|
||||
"env", "react-app"
|
||||
],
|
||||
"plugins": [
|
||||
["import", { "libraryName": "antd", "style": true }]
|
||||
]
|
||||
"presets": [
|
||||
"env", "react-app"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -54,10 +54,11 @@
|
|||
"arrow": "parens-new-line"
|
||||
}
|
||||
],
|
||||
"react/no-did-update-set-state": [0],
|
||||
"react/prefer-stateless-function": 0, // TODO: convert to stateless functions and enable
|
||||
"react/sort-prop-types": 2,
|
||||
"semi": [2, "always"],
|
||||
"sort-imports": [2, { "ignoreCase": true, "memberSyntaxSortOrder": ["single", "multiple", "all", "none"] }],
|
||||
"sort-imports": [2],
|
||||
"space-before-blocks": [2, "always"],
|
||||
"space-before-function-paren": [2, "never"],
|
||||
"strict": [2, "safe"]
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
@import 'styles.css';
|
||||
|
||||
.ant-layout-header {
|
||||
background-color: var(--green);
|
||||
height: 64px;
|
||||
z-index: 100;
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
padding:16px 40px;
|
||||
|
||||
& span.ant-breadcrumb-link, & span.ant-breadcrumb-separator, & :first-child {
|
||||
color: rgb(0, 0, 0, 0.85);
|
||||
font-size: 21px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-layout-header:last-child {
|
||||
color: var(--white);
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
@import 'styles.css';
|
||||
|
||||
.call-to-action {
|
||||
padding: 16px 0;
|
||||
|
||||
& code {
|
||||
background-color: rgba(47, 128, 237, 1);
|
||||
color: white;
|
||||
font-size: .9rem;
|
||||
letter-spacing: 0px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
& .summary {
|
||||
color: var(--green);
|
||||
|
||||
}
|
||||
|
||||
& .action-steps div {
|
||||
line-height: 41px;
|
||||
margin-bottom: 8px;
|
||||
max-width: 272px;
|
||||
height: 41px;
|
||||
|
||||
@media screen and (min-width: 1120px) {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
& .step-container {
|
||||
border-radius: calc(var(--base-width)*4);
|
||||
margin-right: var(--base-width);
|
||||
width: 98%
|
||||
}
|
||||
|
||||
& .step-container > div {
|
||||
float: left;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& .icon-container {
|
||||
width: 37px;
|
||||
border-radius: 0px;
|
||||
padding-left: var(--base-width);
|
||||
}
|
||||
|
||||
& .step-container .fa {
|
||||
font-size: 2em;
|
||||
line-height: 41px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
& .action-steps {
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
|
||||
& .message {
|
||||
color: white;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
& .complete {
|
||||
color: var(--green);
|
||||
background-color: var(--green);
|
||||
}
|
||||
& .incomplete {
|
||||
color: #828282;
|
||||
background-color: #828282;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
@import 'styles.css';
|
||||
|
||||
.octopus-graph {
|
||||
max-width: 940px;
|
||||
padding: calc(var(--base-width) * 2);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: #FAFAFA;
|
||||
|
||||
& .octopus-body {
|
||||
background-color: white;
|
||||
border: 1px solid #F2F2F2;
|
||||
padding: var(--base-width);
|
||||
}
|
||||
|
||||
& .octopus-col {
|
||||
text-align: center;
|
||||
|
||||
& .neighbor {
|
||||
height: 200px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
& .octopus-title {
|
||||
padding: 8px 0;
|
||||
|
||||
&.main-title {
|
||||
font-size: 24px;
|
||||
font-weight: var(--font-weight-extra-bold);
|
||||
}
|
||||
|
||||
&.neighbor-title {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
font-size: 16px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
}
|
||||
|
||||
& .octopus-sr-gauge {
|
||||
padding-top: calc(var(--base-width) * 2);
|
||||
|
||||
& .ant-progress-text {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
& .octopus-metric {
|
||||
line-height: 32px;
|
||||
|
||||
&.status-good {
|
||||
color: var(--green);
|
||||
}
|
||||
&.status-poor {
|
||||
color: var(--red);
|
||||
}
|
||||
&.status-neutral {
|
||||
color: var(--graphgrey);
|
||||
}
|
||||
&.status-ok {
|
||||
color: var(--orange);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
@import 'styles.css';
|
||||
|
||||
.header-with-metric {
|
||||
margin-top: 32px;
|
||||
|
||||
& .subsection-header {
|
||||
float: left;
|
||||
}
|
||||
& .metric {
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.mesh-section .metric-large {
|
||||
width: 131px;
|
||||
}
|
||||
|
||||
.mesh-completion-message {
|
||||
margin-top: calc(4 * var(--base-width));
|
||||
background-color: var(--royalblue);
|
||||
color: white;
|
||||
padding: var(--base-width);
|
||||
font-size: 16;
|
||||
border-radius: calc(var(--base-width)/2);
|
||||
font-weight: var(--font-weight-extra-bold);
|
||||
|
||||
& code {
|
||||
color: #2f80ed;
|
||||
background-color: white;
|
||||
padding: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.service-mesh-table {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.metric-table.mesh-completion-table {
|
||||
& .ant-table-row {
|
||||
& td {
|
||||
& .container-bar {
|
||||
&.neutral {
|
||||
background-color: rgb(130,130,130, 0.1);;
|
||||
}
|
||||
&.poor{
|
||||
background-color: rgb(235, 87, 87, 0.1);
|
||||
}
|
||||
&.good{
|
||||
background-color: rgb(39, 174, 96, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
& .inner-bar {
|
||||
&.neutral {
|
||||
background-color: rgb(130,130,130, 0.8);;
|
||||
}
|
||||
&.poor{
|
||||
background-color: rgba(255, 77, 43, 0.8);
|
||||
}
|
||||
&.good{
|
||||
background-color: rgba(38, 233, 157, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* styles for the StatusTable */
|
||||
td div.status-dot {
|
||||
float: left;
|
||||
margin-right: var(--base-width);
|
||||
|
||||
&.dot-multiline {
|
||||
margin-top: calc(0.5 * var(--base-width));
|
||||
margin-bottom: calc(0.5 * var(--base-width));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* error indicator and modal */
|
||||
.controller-error-icon {
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
color: var(--red);
|
||||
}
|
||||
.controller-init-icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.controller-pod-error {
|
||||
margin-bottom: calc(3 * var(--base-width));
|
||||
|
||||
& .container-error {
|
||||
margin-bottom: calc(3 * var(--base-width));
|
||||
}
|
||||
|
||||
& p {
|
||||
line-height: 8px;
|
||||
}
|
||||
|
||||
& .error-text {
|
||||
padding: var(--base-width);
|
||||
margin-top: var(--base-width);
|
||||
border-radius: calc(0.5 * var(--base-width));
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
background-color: #696969;
|
||||
}
|
||||
}
|
|
@ -1,133 +0,0 @@
|
|||
@import 'styles.css';
|
||||
|
||||
ul.ant-menu.ant-menu-sub {
|
||||
background: var(--sider-black);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: var(--header-black);
|
||||
color: white;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .ant-menu-submenu {
|
||||
& .ant-menu-submenu-title, & .anticon {
|
||||
/* override ant transition animation */
|
||||
transition: none;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
& .sidebar-menu {
|
||||
background: none;
|
||||
}
|
||||
|
||||
& .sidebar-menu-item, & .sidebar-title {
|
||||
font-size: 16px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: white;
|
||||
|
||||
& a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
& .update {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
& .ant-select-selection {
|
||||
width: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
& .sidebar-submenu-item {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
& a {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
& img {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
& .ant-menu-item {
|
||||
& div {
|
||||
& a {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
& span.ant-badge-status {
|
||||
padding-left: 26px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-menu-item:hover {
|
||||
background-color: #007EFF;
|
||||
}
|
||||
|
||||
& .ant-menu-item-active, & .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected {
|
||||
background: #007EFF;
|
||||
|
||||
& a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
& .sidebar-menu-header {
|
||||
height: 60px;
|
||||
padding: var(--base-width) 14px;
|
||||
|
||||
&.collapsed {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
& .version {
|
||||
padding: 0 0 0 16px;
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: 2;
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
& .ant-badge-status-success {
|
||||
background-color: var(--green);
|
||||
}
|
||||
|
||||
& .ant-badge-status-error {
|
||||
background-color: var(--red);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-layout-sider {
|
||||
/* override ant color */
|
||||
background: var(--header-black);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ant-layout .ant-layout-sider .ant-layout-sider-trigger {
|
||||
/* override ant color */
|
||||
background: var(--sider-black);
|
||||
}
|
||||
|
||||
/* Hack to hide the scrollbar for firefox browsers */
|
||||
@-moz-document url-prefix() {
|
||||
.ant-layout-sider-children {
|
||||
margin-right: -15px;
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
@import url('https://use.fontawesome.com/releases/v5.3.1/css/all.css');
|
||||
/* material-ui uses Roboto */
|
||||
@import url("https://fonts.googleapis.com/css?family=Montserrat:400,700|Roboto:100,300,400");
|
||||
|
||||
:root {
|
||||
--white: #fff;
|
||||
--header-black: #1e2322;
|
||||
--sider-black: #343838;
|
||||
--royalblue: #2F80ED;
|
||||
--linkblue: #2196f3;
|
||||
--darkblue: #071E3C;
|
||||
--curiousblue: #2D9CDB;
|
||||
--pictonblue: #56CCF2;
|
||||
|
@ -19,264 +22,48 @@
|
|||
--latency-p99: var(--royalblue);
|
||||
--latency-p95: var(--curiousblue);
|
||||
--latency-p50: var(--pictonblue);
|
||||
--font-stack: 'Lato', helvetica, arial, sans-serif;
|
||||
--font-weight-light: 300;
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-bold: 700;
|
||||
--font-weight-extra-bold: 900;
|
||||
--font-stack: 'Roboto', 'Lato', helvetica, arial, sans-serif;
|
||||
--base-width: 8px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 100%;
|
||||
font-weight: var(--font-weight-regular);
|
||||
font-family: var(--font-stack);
|
||||
line-height: 1.4;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: var(--font-weight-extra-bold);
|
||||
}
|
||||
|
||||
h2, h3, h4, h5, h6 {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.clearfix {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.main {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.page-header .subsection-header {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.time-window-btns {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* center the spinner */
|
||||
.page-content .ant-spin {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 75vh;
|
||||
|
||||
&.short-spin-section {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-section {
|
||||
margin-bottom: calc(var(--base-width) * 3);
|
||||
}
|
||||
|
||||
.subsection-header {
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-extra-bold);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.subsection-header.border-container {
|
||||
height: 48px; /* override height */
|
||||
}
|
||||
|
||||
.subsection-header.border-container-content {
|
||||
height: 32px; /* override height */
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-extra-bold);
|
||||
}
|
||||
|
||||
.summary-info {
|
||||
font-size: 12px;
|
||||
color: var(--neutralgrey);
|
||||
}
|
||||
|
||||
.metric-title {
|
||||
font-size: 12px;
|
||||
font-weight: var(--font-weight-regular);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 16px;
|
||||
font-weight: var(--font-weight-extra-bold);
|
||||
}
|
||||
|
||||
.metric.metric-large {
|
||||
& .metric-title {
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-extra-bold);
|
||||
}
|
||||
& .metric-value {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-msg {
|
||||
font-size: 12px;
|
||||
color: var(--neutralgrey);
|
||||
}
|
||||
|
||||
.action {
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.error-message-container {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
padding: var(--base-width);
|
||||
margin-bottom: calc(3 * var(--base-width));
|
||||
border-radius: 5px;
|
||||
background: var(--red);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
|
||||
& .dismiss {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
}
|
||||
}
|
||||
|
||||
a.button {
|
||||
color: var(--royalblue);
|
||||
background: #fff;
|
||||
border-radius: 48px;
|
||||
border: 1px solid var(--royalblue);
|
||||
padding: 10px 14px 11px 14px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: 14px;
|
||||
margin: 0px 8px 8px 0px;
|
||||
position: relative;
|
||||
line-height: 4;
|
||||
}
|
||||
a.button:hover {
|
||||
top: 1px;
|
||||
background-color: #efefef;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
a.button:active {
|
||||
background-color: var(--royalblue);
|
||||
color: #fff;
|
||||
}
|
||||
a.button.primary {
|
||||
color: #fff;
|
||||
background-color: var(--royalblue);
|
||||
}
|
||||
a.button.primary:hover {
|
||||
top: 1px;
|
||||
background-color: #2875DE;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.button.primary:active {
|
||||
background-color: #2469C7;
|
||||
}
|
||||
color: var(--linkblue);
|
||||
|
||||
/*
|
||||
Ant overrides
|
||||
*/
|
||||
.ant-menu, .ant-table, .ant-tabs {
|
||||
font-family: var(--font-stack);
|
||||
&:focus, &:hover, &:visited, &:link, &:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.metric-table {
|
||||
& .ant-table-content {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
& thead > tr > th {
|
||||
background: var(--white);
|
||||
text-transform: uppercase;
|
||||
font-weight: var(--font-weight-bold);
|
||||
border-bottom: 1px solid;
|
||||
border-color: var(--silver);
|
||||
word-break: keep-all;
|
||||
|
||||
&.ant-table-column-sort {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-table table {
|
||||
font-weight: bold;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
& tr > th, & tr > td {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
& .ant-table-tbody > tr:hover > td {
|
||||
background: rgba(47, 128, 237, .1);
|
||||
}
|
||||
|
||||
& tr .numeric {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
& .metric-table-sr {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
& .metric-table-sr-chart.ant-progress-circle {
|
||||
/* override ant progress circle height */
|
||||
& div.ant-progress-inner {
|
||||
max-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ant Progress bar color orverrides */
|
||||
.success-rate-arc {
|
||||
&.status-good .ant-progress-circle-path {
|
||||
stroke: var(--green);
|
||||
}
|
||||
&.status-ok .ant-progress-circle-path {
|
||||
stroke: var(--orange)
|
||||
}
|
||||
&.status-poor .ant-progress-circle-path {
|
||||
stroke: var(--red);
|
||||
padding-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Colored dot for indicating statuses */
|
||||
div.status-dot {
|
||||
div.status-table-dot {
|
||||
width: calc(2 * var(--base-width));
|
||||
height: calc(2 * var(--base-width));
|
||||
min-width: calc(2 * var(--base-width));
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
div.success-rate-dot {
|
||||
width: calc(1 * var(--base-width));
|
||||
height: calc(1 * var(--base-width));
|
||||
min-width: calc(1 * var(--base-width));
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
&.status-dot-good {
|
||||
background-color: var(--green);
|
||||
}
|
||||
|
@ -284,18 +71,23 @@ div.status-dot {
|
|||
background-color: var(--red);
|
||||
}
|
||||
&.status-dot-neutral {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
&.status-dot-ok {
|
||||
background-color: var(--orange);
|
||||
}
|
||||
&.status-dot-default {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
|
||||
.anticon-check-circle.status-ok {
|
||||
color: var(--green);
|
||||
.breadcrumb-link a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.ant-layout-has-sider {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
.version {
|
||||
max-width: 250px;
|
||||
padding: 24px;
|
||||
font-size: 12px;
|
||||
|
||||
& .new-version-text {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
@import 'styles.css';
|
||||
|
||||
.tap-form {
|
||||
& button.ant-btn-primary {
|
||||
&.tap-ctrl {
|
||||
margin-right: var(--base-width);
|
||||
}
|
||||
|
||||
&.tap-start {
|
||||
background-color: var(--green);
|
||||
border-color: var(--green);
|
||||
}
|
||||
&.tap-stop {
|
||||
background-color: var(--red);
|
||||
border-color: var(--red);
|
||||
}
|
||||
}
|
||||
|
||||
& button.tap-form-toggle {
|
||||
padding-left: 0;
|
||||
border: none;
|
||||
color: rgba(52, 137, 206, 0.906);
|
||||
}
|
||||
}
|
||||
|
||||
.tap-more-info {
|
||||
padding-right: 50px;
|
||||
|
||||
& .tap-info-section {
|
||||
padding: 4px 0;
|
||||
|
||||
& .expand-section-header {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metric-table {
|
||||
& .popover-td {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { friendlyTitle } from './util/Utils.js';
|
||||
import { incompleteMeshMessage } from './util/CopyUtils.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { friendlyTitle } from './util/Utils.js';
|
||||
import { incompleteMeshMessage } from './util/CopyUtils.jsx';
|
||||
|
||||
export default class AddResources extends React.Component {
|
||||
static propTypes = {
|
||||
|
|
|
@ -1,31 +1,79 @@
|
|||
import { Table } from 'antd';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Table from '@material-ui/core/Table';
|
||||
import TableBody from '@material-ui/core/TableBody';
|
||||
import TableCell from '@material-ui/core/TableCell';
|
||||
import TableHead from '@material-ui/core/TableHead';
|
||||
import TableRow from '@material-ui/core/TableRow';
|
||||
import _ from 'lodash';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
// BaseTable extends ant-design's table, but overwrites the `toggleSortOrder`
|
||||
// method, in order to remove the default behavior of unsorting a column when
|
||||
// the same sorting arrow is pressed twice:
|
||||
// https://github.com/ant-design/ant-design/blob/master/components/table/Table.tsx#L348
|
||||
export default class BaseTable extends Table {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
Table.prototype.toggleSortOrder = this.toggleSortOrder;
|
||||
}
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
width: '100%',
|
||||
marginTop: theme.spacing.unit * 3,
|
||||
marginBottom: theme.spacing.unit * 3,
|
||||
overflowX: 'auto',
|
||||
},
|
||||
table: {},
|
||||
});
|
||||
|
||||
toggleSortOrder(order, column) {
|
||||
const newState = {
|
||||
sortOrder: order,
|
||||
sortColumn: column,
|
||||
};
|
||||
function BaseTable(props) {
|
||||
const { classes, tableRows, tableColumns, tableClassName, rowKey} = props;
|
||||
|
||||
if (this.getSortOrderColumns().length === 0) {
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
const onChange = this.props.onChange;
|
||||
if (onChange) {
|
||||
onChange.apply(null, this.prepareParamsArguments({
|
||||
...this.state,
|
||||
...newState,
|
||||
}));
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<Table className={`${classes.table} ${tableClassName}`}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{ _.map(tableColumns, c => (
|
||||
<TableCell
|
||||
key={c.key}
|
||||
numeric={c.isNumeric}>{c.title}
|
||||
</TableCell>
|
||||
))
|
||||
}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{
|
||||
_.map(tableRows, d => {
|
||||
let key = !rowKey ? d.key : rowKey(d);
|
||||
return (
|
||||
<TableRow key={key}>
|
||||
{ _.map(tableColumns, c => (
|
||||
<TableCell
|
||||
key={`table-${key}-${c.key}`}
|
||||
numeric={c.isNumeric}>{c.render(d)}
|
||||
</TableCell>
|
||||
))
|
||||
}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
BaseTable.propTypes = {
|
||||
classes: PropTypes.shape({}).isRequired,
|
||||
rowKey: PropTypes.func,
|
||||
tableClassName: PropTypes.string,
|
||||
tableColumns: PropTypes.arrayOf(PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
isNumeric: PropTypes.bool,
|
||||
render: PropTypes.func
|
||||
})).isRequired,
|
||||
tableRows: PropTypes.arrayOf(PropTypes.shape({}))
|
||||
};
|
||||
|
||||
BaseTable.defaultProps = {
|
||||
rowKey: null,
|
||||
tableClassName: "",
|
||||
tableRows: []
|
||||
};
|
||||
|
||||
export default withStyles(styles)(BaseTable);
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import _ from 'lodash';
|
||||
import { friendlyTitle, isResource, singularResource } from "./util/Utils.js";
|
||||
|
||||
import PropTypes from "prop-types";
|
||||
import React from 'react';
|
||||
import ReactRouterPropTypes from 'react-router-prop-types';
|
||||
import _ from 'lodash';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { Breadcrumb, Layout } from 'antd';
|
||||
import { friendlyTitle, isResource, singularResource } from "./util/Utils.js";
|
||||
import './../../css/breadcrumb-header.css';
|
||||
|
||||
|
||||
const routeToCrumbTitle = {
|
||||
"servicemesh": "Service Mesh",
|
||||
|
@ -16,7 +14,6 @@ const routeToCrumbTitle = {
|
|||
};
|
||||
|
||||
class BreadcrumbHeader extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
api: PropTypes.shape({
|
||||
PrefixedLink: PropTypes.func.isRequired,
|
||||
|
@ -73,11 +70,11 @@ class BreadcrumbHeader extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
renderBreadcrumbSegment(segment, index) {
|
||||
renderBreadcrumbSegment(segment, shouldPluralizeFirstSegment) {
|
||||
let isMeshResource = isResource(segment);
|
||||
|
||||
if (isMeshResource) {
|
||||
if (index === 0) {
|
||||
if (!shouldPluralizeFirstSegment) {
|
||||
return friendlyTitle(segment).singular;
|
||||
}
|
||||
return this.segmentToFriendlyTitle(segment, true);
|
||||
|
@ -85,46 +82,23 @@ class BreadcrumbHeader extends React.Component {
|
|||
return this.segmentToFriendlyTitle(segment, false);
|
||||
}
|
||||
|
||||
renderBreadcrumbs(breadcrumbs) {
|
||||
let PrefixedLink = this.api.PrefixedLink;
|
||||
|
||||
if (breadcrumbs.length === 1) {
|
||||
// Check for a single segment so that we can pluralize it.
|
||||
let singleBreadcrumb = breadcrumbs[0];
|
||||
|
||||
return (
|
||||
<Breadcrumb.Item key={singleBreadcrumb.segment}>
|
||||
<PrefixedLink
|
||||
to={singleBreadcrumb.link}>
|
||||
{this.renderBreadcrumbSegment(singleBreadcrumb.segment)}
|
||||
</PrefixedLink>
|
||||
</Breadcrumb.Item>
|
||||
);
|
||||
} else {
|
||||
return _.map(breadcrumbs, (pathSegment, index) => {
|
||||
return (
|
||||
<Breadcrumb.Item key={pathSegment.segment}>
|
||||
<PrefixedLink
|
||||
to={pathSegment.link}>
|
||||
{this.renderBreadcrumbSegment(pathSegment.segment, index)}
|
||||
</PrefixedLink>
|
||||
</Breadcrumb.Item>
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let prefix = this.props.pathPrefix;
|
||||
let PrefixedLink = this.api.PrefixedLink;
|
||||
let breadcrumbs = this.convertURLToBreadcrumbs(this.props.location.pathname.replace(prefix, ""));
|
||||
let shouldPluralizeFirstSegment = breadcrumbs.length === 1;
|
||||
|
||||
return (
|
||||
<Layout.Header>
|
||||
<Breadcrumb separator=">">
|
||||
{ this.renderBreadcrumbs(breadcrumbs) }
|
||||
</Breadcrumb>
|
||||
</Layout.Header>
|
||||
);
|
||||
return _.map(breadcrumbs, (pathSegment, index) => {
|
||||
return (
|
||||
<span key={pathSegment.segment} className="breadcrumb-link">
|
||||
<PrefixedLink
|
||||
to={pathSegment.link}>
|
||||
{this.renderBreadcrumbSegment(pathSegment.segment, shouldPluralizeFirstSegment && index === 0)}
|
||||
</PrefixedLink>
|
||||
{ index < _.size(breadcrumbs) - 1 ? " > " : null}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,49 +1,73 @@
|
|||
import { incompleteMeshMessage } from './util/CopyUtils.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import './../../css/cta.css';
|
||||
import Step from '@material-ui/core/Step';
|
||||
import StepContent from '@material-ui/core/StepContent';
|
||||
import StepLabel from '@material-ui/core/StepLabel';
|
||||
import Stepper from '@material-ui/core/Stepper';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import _ from 'lodash';
|
||||
import { incompleteMeshMessage } from './util/CopyUtils.jsx';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
const CallToAction = ({resource, numResources}) => (
|
||||
<div className="call-to-action">
|
||||
<div className="action summary">The service mesh was successfully installed!</div>
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
width: '90%',
|
||||
},
|
||||
button: {
|
||||
marginRight: theme.spacing.unit,
|
||||
},
|
||||
instructions: {
|
||||
marginTop: theme.spacing.unit,
|
||||
marginBottom: theme.spacing.unit,
|
||||
},
|
||||
});
|
||||
|
||||
<div className="action-steps">
|
||||
<div className="step-container complete">
|
||||
<div className="icon-container">
|
||||
<i className="fa fa-check-circle" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="message"><p>Controller successfully installed</p></div>
|
||||
</div>
|
||||
function getSteps(numResources, resource) {
|
||||
return [
|
||||
{ label: 'Controller successfully installed' },
|
||||
{ label: `${numResources ? numResources : 'No'} ${resource}s detected` },
|
||||
{ label: `Connect your first ${resource}`, content: incompleteMeshMessage() }
|
||||
];
|
||||
}
|
||||
|
||||
<div className="step-container complete">
|
||||
<div className="icon-container">
|
||||
<i className="fa fa-check-circle" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="message">{numResources ? numResources : 'No'} {resource}s detected</div>
|
||||
</div>
|
||||
class CallToAction extends React.Component {
|
||||
render() {
|
||||
const { resource, numResources } = this.props;
|
||||
const steps = getSteps(numResources, resource);
|
||||
const lastStep = _.size(steps) - 1; // hardcode the last step as the active step
|
||||
|
||||
<div className="step-container incomplete">
|
||||
<div className="icon-container">
|
||||
<i className="fa fa-circle-o" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="message">Connect your first {resource}</div>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Typography>The service mesh was successfully installed!</Typography>
|
||||
<Stepper
|
||||
activeStep={lastStep}
|
||||
orientation="vertical">
|
||||
{
|
||||
steps.map((step, i) => {
|
||||
const props = {};
|
||||
props.completed = i < lastStep; // select the last step as the currently active one
|
||||
|
||||
<div className="clearfix">
|
||||
{incompleteMeshMessage()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
CallToAction.defaultProps = {
|
||||
numResources: 0,
|
||||
resource: 'resource',
|
||||
};
|
||||
return (
|
||||
<Step key={step.label} {...props}>
|
||||
<StepLabel>{step.label}</StepLabel>
|
||||
<StepContent>{step.content}</StepContent>
|
||||
</Step>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Stepper>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CallToAction.propTypes = {
|
||||
numResources: PropTypes.number,
|
||||
resource: PropTypes.string,
|
||||
resource: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CallToAction;
|
||||
CallToAction.defaultProps = {
|
||||
numResources: null
|
||||
};
|
||||
|
||||
export default withStyles(styles)(CallToAction);
|
||||
|
|
|
@ -1,63 +1,103 @@
|
|||
import _ from 'lodash';
|
||||
import { apiErrorPropType } from './util/ApiHelpers.jsx';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Col, Row } from 'antd';
|
||||
import Snackbar from '@material-ui/core/Snackbar';
|
||||
import WarningIcon from '@material-ui/icons/Warning';
|
||||
import _ from 'lodash';
|
||||
import { apiErrorPropType } from './util/ApiHelpers.jsx';
|
||||
import classNames from 'classnames';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
class ErrorMessage extends React.Component {
|
||||
static defaultProps = {
|
||||
message: {
|
||||
status: null,
|
||||
statusText: "An error occured",
|
||||
url: "",
|
||||
error: ""
|
||||
},
|
||||
onHideMessage: _.noop
|
||||
}
|
||||
const defaultMessage = "An error has occurred.";
|
||||
|
||||
static propTypes = {
|
||||
message: apiErrorPropType,
|
||||
onHideMessage: PropTypes.func
|
||||
}
|
||||
const styles = theme => ({
|
||||
close: {
|
||||
padding: theme.spacing.unit / 2,
|
||||
},
|
||||
error: {
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
},
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
iconVariant: {
|
||||
opacity: 0.9,
|
||||
marginRight: theme.spacing.unit,
|
||||
},
|
||||
margin: {
|
||||
margin: theme.spacing.unit,
|
||||
},
|
||||
message: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.hideMessage = this.hideMessage.bind(this);
|
||||
this.state = {
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
class ErrorSnackbar extends React.Component {
|
||||
state = {
|
||||
open: true,
|
||||
};
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (!_.isEmpty(newProps.message)) {
|
||||
this.setState({ visible :true });
|
||||
}
|
||||
}
|
||||
|
||||
hideMessage() {
|
||||
this.props.onHideMessage();
|
||||
this.setState({
|
||||
visible: false
|
||||
});
|
||||
}
|
||||
handleClose = () => {
|
||||
this.setState({ open: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
const { statusText, error, url, status } = this.props.message;
|
||||
return !this.state.visible ? null : (
|
||||
<Row gutter={0}>
|
||||
<div className="error-message-container">
|
||||
<Col span={20}>
|
||||
{ !status && !statusText ? null : <div>{status} {statusText}</div> }
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
open={this.state.open}
|
||||
autoHideDuration={6000}
|
||||
onClose={this.handleClose}
|
||||
ContentProps={{
|
||||
'aria-describedby': 'message-id',
|
||||
headlineMapping: { // https://github.com/mui-org/material-ui/issues/13144
|
||||
body1: "div",
|
||||
body2: "div"
|
||||
},
|
||||
className: classNames(classes.error, classes.margin)
|
||||
}}
|
||||
message={(
|
||||
<div id="message-id" >
|
||||
<div className={classes.message}>
|
||||
<WarningIcon className={classNames(classes.icon, classes.iconVariant)} />
|
||||
{ !status ? null : status + " " }{ _.isEmpty(statusText) ? defaultMessage : statusText }
|
||||
</div>
|
||||
{ !error ? null : <div>{error}</div> }
|
||||
{ !url ? null : <div>{url}</div> }
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<div className="dismiss" onClick={this.hideMessage} role="presentation">Dismiss X</div>
|
||||
</Col>
|
||||
</div>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
action={[
|
||||
<IconButton
|
||||
key="close"
|
||||
aria-label="Close"
|
||||
color="inherit"
|
||||
className={classes.close}
|
||||
onClick={this.handleClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>,
|
||||
]} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorMessage;
|
||||
ErrorSnackbar.propTypes = {
|
||||
classes: PropTypes.shape({}).isRequired,
|
||||
message: apiErrorPropType,
|
||||
};
|
||||
|
||||
ErrorSnackbar.defaultProps = {
|
||||
message: {
|
||||
status: null,
|
||||
statusText: defaultMessage,
|
||||
url: "",
|
||||
error: ""
|
||||
}
|
||||
};
|
||||
|
||||
export default withStyles(styles)(ErrorSnackbar);
|
||||
|
|
|
@ -1,41 +1,36 @@
|
|||
import _ from 'lodash';
|
||||
import { friendlyTitle } from './util/Utils.js';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import ErrorIcon from '@material-ui/icons/Error';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Button, Icon, Modal, Switch, Tooltip } from 'antd';
|
||||
import Switch from '@material-ui/core/Switch';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import _ from 'lodash';
|
||||
import { friendlyTitle } from './util/Utils.js';
|
||||
|
||||
// max characters we display for error messages before truncating them
|
||||
const maxErrorLength = 500;
|
||||
|
||||
export default class ErrorModal extends React.Component {
|
||||
static propTypes = {
|
||||
errors: PropTypes.shape({}).isRequired,
|
||||
resourceName: PropTypes.string.isRequired,
|
||||
resourceType: PropTypes.string.isRequired
|
||||
}
|
||||
const maxErrorLength = 50;
|
||||
|
||||
class ErrorModal extends React.Component {
|
||||
state = {
|
||||
visible: false,
|
||||
truncateErrors: true
|
||||
}
|
||||
truncateErrors: true,
|
||||
open: false,
|
||||
scroll: 'paper',
|
||||
};
|
||||
|
||||
showModal = () => {
|
||||
this.setState({
|
||||
visible: true,
|
||||
});
|
||||
}
|
||||
handleClickOpen = () => {
|
||||
this.setState({ open: true });
|
||||
};
|
||||
|
||||
handleOk = () => {
|
||||
this.setState({
|
||||
visible: false,
|
||||
});
|
||||
}
|
||||
|
||||
handleCancel = () => {
|
||||
this.setState({
|
||||
visible: false,
|
||||
});
|
||||
}
|
||||
handleClose = () => {
|
||||
this.setState({ open: false });
|
||||
};
|
||||
|
||||
toggleTruncateErrors = () => {
|
||||
this.setState({
|
||||
|
@ -85,10 +80,20 @@ export default class ErrorModal extends React.Component {
|
|||
|
||||
return _.map(errorsByContainer, (errors, container) => (
|
||||
<div key={`error-${container}`} className="container-error">
|
||||
<div className="clearfix">
|
||||
<span className="pull-left" title="container name">{container}</span>
|
||||
<span className="pull-right" title="docker image">{_.get(errors, [0, "image"])}</span>
|
||||
</div>
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justify="space-between"
|
||||
alignItems="center">
|
||||
<Grid item>
|
||||
<Typography variant="subtitle1" gutterBottom>{container}</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="subtitle1" gutterBottom align="right">
|
||||
{_.get(errors, [0, "image"])}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<div className="error-text">
|
||||
{
|
||||
|
@ -117,14 +122,14 @@ export default class ErrorModal extends React.Component {
|
|||
return _.map(errors, err => {
|
||||
return (
|
||||
<div className="controller-pod-error" key={err.pod}>
|
||||
<h3 title="pod name">{err.pod}</h3>
|
||||
<Typography variant="title" gutterBottom>{err.pod}</Typography>
|
||||
{this.renderContainerErrors(err.pod, err.byContainer)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderIconStatus = errors => {
|
||||
renderStatusIcon = errors => {
|
||||
let showInit = true;
|
||||
|
||||
_.each(errors.byPodAndContainer, container => {
|
||||
|
@ -137,41 +142,60 @@ export default class ErrorModal extends React.Component {
|
|||
|
||||
if (showInit) {
|
||||
return (
|
||||
<Tooltip
|
||||
title="Pods are initializing"
|
||||
overlayStyle={{ fontSize: "12px" }}>
|
||||
<Icon type="loading" theme="outlined" className="controller-init-icon" />
|
||||
</Tooltip>
|
||||
<Tooltip title="Pods are initializing"><CircularProgress size={20} thickness={4} /></Tooltip>
|
||||
);
|
||||
} else {
|
||||
return <Icon type="warning" className="controller-error-icon" onClick={this.showModal} />;
|
||||
return <ErrorIcon onClick={this.handleClickOpen} />;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let errors = this.processErrorData(this.props.errors);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.renderIconStatus(errors)}
|
||||
<Modal
|
||||
className="controller-pod-error-modal"
|
||||
title={`Errors in ${friendlyTitle(this.props.resourceType).singular} ${this.props.resourceName}`}
|
||||
visible={this.state.visible}
|
||||
onOk={this.handleOk}
|
||||
onCancel={this.handleCancel}
|
||||
footer={<Button key="modal-ok" type="primary" onClick={this.handleOk}>OK</Button>}
|
||||
width="800px">
|
||||
<React.Fragment>
|
||||
<div>
|
||||
{this.renderStatusIcon(errors)}
|
||||
<Dialog
|
||||
open={this.state.open}
|
||||
onClose={this.handleClose}
|
||||
scroll={this.state.scroll}
|
||||
aria-labelledby="scroll-dialog-title">
|
||||
|
||||
<DialogTitle id="scroll-dialog-title">Errors in {friendlyTitle(this.props.resourceType).singular} {this.props.resourceName}</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
{
|
||||
errors.shouldTruncate ?
|
||||
<React.Fragment>
|
||||
Some of these error messages are very long. Show full error text? <Switch onChange={this.toggleTruncateErrors} />
|
||||
</React.Fragment> : null
|
||||
!errors.shouldTruncate ? null :
|
||||
<React.Fragment>
|
||||
Some of these error messages are very long. Show full error text?
|
||||
<Switch
|
||||
checked={!this.state.truncateErrors}
|
||||
onChange={this.toggleTruncateErrors}
|
||||
color="primary" />
|
||||
</React.Fragment>
|
||||
}
|
||||
{this.renderPodErrors(errors.byPodAndContainer)}
|
||||
</React.Fragment>
|
||||
</Modal>
|
||||
</React.Fragment>
|
||||
|
||||
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={this.handleClose} color="primary">Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorModal.propTypes = {
|
||||
errors: PropTypes.shape({}),
|
||||
resourceName: PropTypes.string.isRequired,
|
||||
resourceType: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
ErrorModal.defaultProps = {
|
||||
errors: {}
|
||||
};
|
||||
|
||||
export default ErrorModal;
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
import Button from '@material-ui/core/Button';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Table from '@material-ui/core/Table';
|
||||
import TableBody from '@material-ui/core/TableBody';
|
||||
import TableCell from '@material-ui/core/TableCell';
|
||||
import TableHead from '@material-ui/core/TableHead';
|
||||
import TableRow from '@material-ui/core/TableRow';
|
||||
import _ from 'lodash';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
width: '100%',
|
||||
marginTop: theme.spacing.unit * 3,
|
||||
overflowX: 'auto',
|
||||
},
|
||||
table: {
|
||||
minWidth: 700
|
||||
},
|
||||
});
|
||||
|
||||
class ExpandableTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
open: false,
|
||||
datum: {}
|
||||
};
|
||||
}
|
||||
|
||||
handleDialogOpen = d => () => {
|
||||
this.setState({ open: true, datum: d });
|
||||
}
|
||||
|
||||
handleDialogClose = () => {
|
||||
this.setState({ open: false, datum: {} });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { expandedRowRender, classes, tableRows, tableColumns, tableClassName } = this.props;
|
||||
let columns = _.concat([{
|
||||
title: " ",
|
||||
key: "expansion",
|
||||
render: d => {
|
||||
return (
|
||||
<IconButton onClick={this.handleDialogOpen(d)}><ExpandMoreIcon /></IconButton>
|
||||
);
|
||||
}
|
||||
}], tableColumns);
|
||||
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<Table className={`${classes.table} ${tableClassName}`}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{
|
||||
_.map(columns, c => (
|
||||
<TableCell
|
||||
key={c.key}
|
||||
numeric={c.isNumeric}>{c.title}
|
||||
</TableCell>
|
||||
)
|
||||
)
|
||||
}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{ _.map(tableRows, d => {
|
||||
return (
|
||||
<React.Fragment key={"frag-" + d.key}>
|
||||
<TableRow
|
||||
key={d.key}
|
||||
onClick={this.handleClick}
|
||||
ref={ref => {
|
||||
this.container = ref;
|
||||
}}>
|
||||
{
|
||||
_.map(columns, c => (
|
||||
<TableCell
|
||||
key={`table-${d.key}-${c.key}`}
|
||||
numeric={c.isNumeric}>
|
||||
{c.render(d)}
|
||||
</TableCell>
|
||||
))
|
||||
}
|
||||
</TableRow>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Dialog
|
||||
maxWidth="md"
|
||||
open={this.state.open}
|
||||
onClose={this.handleDialogClose}
|
||||
aria-labelledby="form-dialog-title">
|
||||
<DialogTitle id="form-dialog-title">Request Details</DialogTitle>
|
||||
<DialogContent>
|
||||
{expandedRowRender(this.state.datum)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={this.handleDialogClose} color="primary">Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ExpandableTable.propTypes = {
|
||||
classes: PropTypes.shape({}).isRequired,
|
||||
expandedRowRender: PropTypes.func.isRequired,
|
||||
tableClassName: PropTypes.string,
|
||||
tableColumns: PropTypes.arrayOf(PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
isNumeric: PropTypes.bool,
|
||||
render: PropTypes.func
|
||||
})).isRequired,
|
||||
tableRows: PropTypes.arrayOf(PropTypes.shape({}))
|
||||
};
|
||||
|
||||
ExpandableTable.defaultProps = {
|
||||
tableClassName: "",
|
||||
tableRows: []
|
||||
};
|
||||
|
||||
export default withStyles(styles)(ExpandableTable);
|
|
@ -1,6 +1,6 @@
|
|||
import { grafanaIcon } from './util/SvgWrappers.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { grafanaIcon } from './util/SvgWrappers.jsx';
|
||||
|
||||
const GrafanaLink = ({PrefixedLink, name, namespace, resource}) => {
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import BaseTable from './BaseTable.jsx';
|
||||
import ErrorModal from './ErrorModal.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { StyledProgress } from './util/Progress.jsx';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import _ from 'lodash';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
|
||||
const getClassification = (meshedPodCount, failedPodCount) => {
|
||||
if (failedPodCount > 0) {
|
||||
return "poor";
|
||||
} else if (meshedPodCount === 0) {
|
||||
return "neutral";
|
||||
} else {
|
||||
return "good";
|
||||
}
|
||||
};
|
||||
|
||||
const namespacesColumns = PrefixedLink => [
|
||||
{
|
||||
title: "Namespace",
|
||||
key: "namespace",
|
||||
render: d => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PrefixedLink to={"/namespaces/" + d.namespace}>{d.namespace}</PrefixedLink>
|
||||
{ _.isEmpty(d.errors) ? null :
|
||||
<ErrorModal errors={d.errors} resourceName={d.namespace} resourceType="namespace" />
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Meshed pods",
|
||||
key: "meshedPodsStr",
|
||||
isNumeric: true,
|
||||
render: d => d.meshedPodsStr
|
||||
},
|
||||
{
|
||||
title: "Meshed Status",
|
||||
key: "meshification",
|
||||
render: row => {
|
||||
let percent = row.meshedPercent.get();
|
||||
let barType = _.isEmpty(row.errors) ?
|
||||
getClassification(row.meshedPods, row.failedPods) : "poor";
|
||||
let Progress = StyledProgress(barType);
|
||||
|
||||
let percentMeshedMsg = "";
|
||||
if (row.meshedPercent.get() >= 0) {
|
||||
percentMeshedMsg = `(${row.meshedPercent.prettyRate()})`;
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
title={(
|
||||
<div>
|
||||
<div>
|
||||
{`${row.meshedPods} out of ${row.totalPods} running or pending pods are in the mesh ${percentMeshedMsg}`}
|
||||
</div>
|
||||
{row.failedPods === 0 ? null : <div>{ `${row.failedPods} failed pods` }</div>}
|
||||
</div>
|
||||
)}>
|
||||
<Progress variant="determinate" value={Math.round(percent * 100)} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
class MeshedStatusTable extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<BaseTable
|
||||
tableClassName="metric-table mesh-completion-table"
|
||||
tableRows={this.props.tableRows}
|
||||
tableColumns={namespacesColumns(this.props.api.PrefixedLink)}
|
||||
rowKey={d => d.namespace} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MeshedStatusTable.propTypes = {
|
||||
api: PropTypes.shape({
|
||||
PrefixedLink: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
tableRows: PropTypes.arrayOf(PropTypes.shape({}))
|
||||
};
|
||||
|
||||
MeshedStatusTable.defaultProps = {
|
||||
tableRows: []
|
||||
};
|
||||
|
||||
export default withContext(MeshedStatusTable);
|
|
@ -1,17 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
const Metric = ({className, title, value}) => (
|
||||
<div className={`metric ${className || ""}`}>
|
||||
<div className="metric-title">{title}</div>
|
||||
<div className="metric-value">{value}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Metric.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
value: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default Metric;
|
|
@ -1,70 +1,31 @@
|
|||
import _ from 'lodash';
|
||||
import { friendlyTitle, metricToFormatter } from './util/Utils.js';
|
||||
import { processedMetricsPropType, successRateWithMiniChart } from './util/MetricUtils.jsx';
|
||||
|
||||
import BaseTable from './BaseTable.jsx';
|
||||
import ErrorModal from './ErrorModal.jsx';
|
||||
import GrafanaLink from './GrafanaLink.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import {
|
||||
friendlyTitle,
|
||||
metricToFormatter,
|
||||
numericSort
|
||||
} from './util/Utils.js';
|
||||
import { processedMetricsPropType, successRateWithMiniChart } from './util/MetricUtils.jsx';
|
||||
|
||||
/*
|
||||
Table to display Success Rate, Requests and Latency in tabs.
|
||||
Expects rollup and timeseries data.
|
||||
*/
|
||||
const smMetricColWidth = "70px";
|
||||
|
||||
const formatTitle = (title, tooltipText) => {
|
||||
if (!tooltipText) {
|
||||
return title;
|
||||
} else {
|
||||
return (
|
||||
<Tooltip
|
||||
title={tooltipText}
|
||||
overlayStyle={{ fontSize: "12px" }}>
|
||||
{title}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const meshedColumn = {
|
||||
title: formatTitle("Meshed"),
|
||||
dataIndex: "pods",
|
||||
key: "pods",
|
||||
className: "numeric",
|
||||
sorter: (a, b) => numericSort(a.pods.totalPods, b.pods.totalPods),
|
||||
render: p => p.meshedPods + "/" + p.totalPods
|
||||
};
|
||||
|
||||
const columnDefinitions = (resource, namespaces, onFilterClick, showNamespaceColumn, PrefixedLink) => {
|
||||
const columnDefinitions = (resource, showNamespaceColumn, PrefixedLink) => {
|
||||
let isAuthorityTable = resource === "authority";
|
||||
|
||||
let nsColumn = [
|
||||
{
|
||||
title: formatTitle("Namespace"),
|
||||
title: "Namespace",
|
||||
key: "namespace",
|
||||
dataIndex: "namespace",
|
||||
filters: namespaces,
|
||||
onFilterDropdownVisibleChange: onFilterClick,
|
||||
onFilter: (value, row) => row.namespace.indexOf(value) === 0,
|
||||
sorter: (a, b) => (a.namespace || "").localeCompare(b.namespace),
|
||||
render: ns => !ns ? "---" : <PrefixedLink to={"/namespaces/" + ns}>{ns}</PrefixedLink>
|
||||
isNumeric: false,
|
||||
render: d => !d.namespace ? "---" : <PrefixedLink to={"/namespaces/" + d.namespace}>{d.namespace}</PrefixedLink>
|
||||
}
|
||||
];
|
||||
|
||||
let grafanaLinkColumn = [
|
||||
{
|
||||
title: formatTitle("Dash", "Grafana Dashboard"),
|
||||
title: "Grafana Dashboard",
|
||||
key: "grafanaDashboard",
|
||||
className: "numeric",
|
||||
width: smMetricColWidth,
|
||||
isNumeric: true,
|
||||
render: row => !row.added || _.get(row, "pods.totalPods") === "0" ? null : (
|
||||
<GrafanaLink
|
||||
name={row.name}
|
||||
|
@ -75,90 +36,78 @@ const columnDefinitions = (resource, namespaces, onFilterClick, showNamespaceCol
|
|||
}
|
||||
];
|
||||
|
||||
let meshedColumn = {
|
||||
title: "Meshed",
|
||||
key: "meshed",
|
||||
isNumeric: true,
|
||||
render: d => !d.pods ? null : d.pods.meshedPods + "/" + d.pods.totalPods
|
||||
};
|
||||
|
||||
let columns = [
|
||||
{
|
||||
title: formatTitle(friendlyTitle(resource).singular),
|
||||
key: "name",
|
||||
defaultSortOrder: 'ascend',
|
||||
sorter: (a, b) => (a.name || "").localeCompare(b.name),
|
||||
render: row => {
|
||||
title: friendlyTitle(resource).singular,
|
||||
key: "resource-title",
|
||||
isNumeric: false,
|
||||
render: d => {
|
||||
let nameContents;
|
||||
if (resource === "namespace") {
|
||||
nameContents = <PrefixedLink to={"/namespaces/" + row.name}>{row.name}</PrefixedLink>;
|
||||
} else if (!row.added || isAuthorityTable) {
|
||||
nameContents = row.name;
|
||||
nameContents = <PrefixedLink to={"/namespaces/" + d.name}>{d.name}</PrefixedLink>;
|
||||
} else if (!d.added || isAuthorityTable) {
|
||||
nameContents = d.name;
|
||||
} else {
|
||||
nameContents = (
|
||||
<PrefixedLink to={"/namespaces/" + row.namespace + "/" + resource + "s/" + row.name}>
|
||||
{row.name}
|
||||
<PrefixedLink to={"/namespaces/" + d.namespace + "/" + resource + "s/" + d.name}>
|
||||
{d.name}
|
||||
</PrefixedLink>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
{nameContents}
|
||||
{ _.isEmpty(row.errors) ? null : <ErrorModal errors={row.errors} resourceName={row.name} resourceType={resource} /> }
|
||||
{ _.isEmpty(d.errors) ? null : <ErrorModal errors={d.errors} resourceName={d.name} resourceType={resource} /> }
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: formatTitle("SR", "Success Rate"),
|
||||
dataIndex: "successRate",
|
||||
key: "successRateRollup",
|
||||
className: "numeric",
|
||||
width: "120px",
|
||||
sorter: (a, b) => numericSort(a.successRate, b.successRate),
|
||||
render: successRateWithMiniChart
|
||||
title: "Success Rate",
|
||||
key: "success-rate",
|
||||
isNumeric: true,
|
||||
render: d => successRateWithMiniChart(d.successRate)
|
||||
},
|
||||
{
|
||||
title: formatTitle("RPS", "Request Rate"),
|
||||
dataIndex: "requestRate",
|
||||
key: "requestRateRollup",
|
||||
className: "numeric",
|
||||
width: smMetricColWidth,
|
||||
sorter: (a, b) => numericSort(a.requestRate, b.requestRate),
|
||||
render: metricToFormatter["NO_UNIT"]
|
||||
title: "Request Rate",
|
||||
key: "request-rate",
|
||||
isNumeric: true,
|
||||
render: d => metricToFormatter["NO_UNIT"](d.requestRate)
|
||||
},
|
||||
{
|
||||
title: formatTitle("P50", "P50 Latency"),
|
||||
dataIndex: "P50",
|
||||
key: "p50LatencyRollup",
|
||||
className: "numeric",
|
||||
width: smMetricColWidth,
|
||||
sorter: (a, b) => numericSort(a.P50, b.P50),
|
||||
render: metricToFormatter["LATENCY"]
|
||||
title: "P50 Latency",
|
||||
key: "p50_latency",
|
||||
isNumeric: true,
|
||||
render: d => metricToFormatter["LATENCY"](d.P50)
|
||||
},
|
||||
{
|
||||
title: formatTitle("P95", "P95 Latency"),
|
||||
dataIndex: "P95",
|
||||
key: "p95LatencyRollup",
|
||||
className: "numeric",
|
||||
width: smMetricColWidth,
|
||||
sorter: (a, b) => numericSort(a.P95, b.P95),
|
||||
render: metricToFormatter["LATENCY"]
|
||||
title: "P95 Latency",
|
||||
key: "p95_latency",
|
||||
isNumeric: true,
|
||||
render: d => metricToFormatter["LATENCY"](d.P95)
|
||||
},
|
||||
{
|
||||
title: formatTitle("P99", "P99 Latency"),
|
||||
dataIndex: "P99",
|
||||
key: "p99LatencyRollup",
|
||||
className: "numeric",
|
||||
width: smMetricColWidth,
|
||||
sorter: (a, b) => numericSort(a.P99, b.P99),
|
||||
render: metricToFormatter["LATENCY"]
|
||||
title: "P99 Latency",
|
||||
key: "p99_latency",
|
||||
isNumeric: true,
|
||||
render: d => metricToFormatter["LATENCY"](d.P99)
|
||||
},
|
||||
{
|
||||
title: formatTitle("TLS", "Percentage of TLS Traffic"),
|
||||
key: "tlsTraffic",
|
||||
dataIndex: "tlsRequestPercent",
|
||||
className: "numeric",
|
||||
width: smMetricColWidth,
|
||||
sorter: (a, b) => numericSort(a.tlsRequestPercent.get(), b.tlsRequestPercent.get()),
|
||||
render: d => _.isNil(d) || d.get() === -1 ? "---" : d.prettyRate()
|
||||
title: "TLS",
|
||||
key: "has_tls",
|
||||
isNumeric: true,
|
||||
render: d => _.isNil(d.tlsRequestPercent) || d.tlsRequestPercent.get() === -1 ? "---" : d.tlsRequestPercent.prettyRate()
|
||||
}
|
||||
];
|
||||
|
||||
// don't add the meshed column on a Authority MetricsTable
|
||||
// don't add the meshed column on a Authority MetricsTable
|
||||
if (!isAuthorityTable) {
|
||||
columns.splice(1, 0, meshedColumn);
|
||||
columns = _.concat(columns, grafanaLinkColumn);
|
||||
|
@ -171,13 +120,20 @@ const columnDefinitions = (resource, namespaces, onFilterClick, showNamespaceCol
|
|||
}
|
||||
};
|
||||
|
||||
/** @extends React.Component */
|
||||
export class MetricsTableBase extends BaseTable {
|
||||
static defaultProps = {
|
||||
showNamespaceColumn: true,
|
||||
metrics: []
|
||||
}
|
||||
|
||||
const preprocessMetrics = metrics => {
|
||||
let tableData = _.cloneDeep(metrics);
|
||||
|
||||
_.each(tableData, datum => {
|
||||
_.each(datum.latency, (value, quantile) => {
|
||||
datum[quantile] = value;
|
||||
});
|
||||
});
|
||||
|
||||
return tableData;
|
||||
};
|
||||
|
||||
class MetricsTable extends React.Component {
|
||||
static propTypes = {
|
||||
api: PropTypes.shape({
|
||||
PrefixedLink: PropTypes.func.isRequired,
|
||||
|
@ -185,81 +141,27 @@ export class MetricsTableBase extends BaseTable {
|
|||
metrics: PropTypes.arrayOf(processedMetricsPropType),
|
||||
resource: PropTypes.string.isRequired,
|
||||
showNamespaceColumn: PropTypes.bool
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = this.props.api;
|
||||
this.onFilterDropdownVisibleChange = this.onFilterDropdownVisibleChange.bind(this);
|
||||
this.state = {
|
||||
preventTableUpdates: false
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
// prevent the table from updating if the filter dropdown menu is open
|
||||
// this is because if the table updates, the filters will reset which
|
||||
// makes it impossible to select a filter
|
||||
return !this.state.preventTableUpdates;
|
||||
}
|
||||
|
||||
onFilterDropdownVisibleChange(dropdownVisible) {
|
||||
this.setState({ preventTableUpdates: dropdownVisible});
|
||||
}
|
||||
|
||||
preprocessMetrics() {
|
||||
let tableData = _.cloneDeep(this.props.metrics);
|
||||
let namespaces = [];
|
||||
|
||||
_.each(tableData, datum => {
|
||||
namespaces.push(datum.namespace);
|
||||
_.each(datum.latency, (value, quantile) => {
|
||||
datum[quantile] = value;
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
rows: tableData,
|
||||
namespaces: _.uniq(namespaces)
|
||||
};
|
||||
}
|
||||
static defaultProps = {
|
||||
showNamespaceColumn: true,
|
||||
metrics: []
|
||||
};
|
||||
|
||||
render() {
|
||||
let tableData = this.preprocessMetrics();
|
||||
let namespaceFilterText = _.map(tableData.namespaces, ns => {
|
||||
return { text: ns, value: ns };
|
||||
});
|
||||
const { metrics, resource, showNamespaceColumn, api } = this.props;
|
||||
|
||||
let resource = this.props.resource;
|
||||
|
||||
let showNsColumn = this.props.showNamespaceColumn;
|
||||
if (resource === "namespace") {
|
||||
showNsColumn = false;
|
||||
}
|
||||
|
||||
let columns = _.compact(columnDefinitions(
|
||||
resource,
|
||||
namespaceFilterText,
|
||||
this.onFilterDropdownVisibleChange,
|
||||
showNsColumn,
|
||||
this.api.PrefixedLink
|
||||
));
|
||||
|
||||
let locale = {
|
||||
emptyText: `No ${friendlyTitle(resource).plural} detected.`
|
||||
};
|
||||
let showNsColumn = resource === "namespace" ? false : showNamespaceColumn;
|
||||
|
||||
let columns = columnDefinitions(resource, showNsColumn, api.PrefixedLink);
|
||||
let rows = preprocessMetrics(metrics);
|
||||
return (
|
||||
<BaseTable
|
||||
dataSource={tableData.rows}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
className="metric-table"
|
||||
rowKey={r => `${r.namespace}/${r.name}`}
|
||||
locale={locale}
|
||||
size="middle" />
|
||||
tableRows={rows}
|
||||
tableColumns={columns}
|
||||
tableClassName="metric-table" />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withContext(MetricsTableBase);
|
||||
export default withContext(MetricsTable);
|
||||
|
|
|
@ -1,74 +1,64 @@
|
|||
import _ from 'lodash';
|
||||
import ApiHelpers from './util/ApiHelpers.jsx';
|
||||
import BaseTable from './BaseTable.jsx';
|
||||
import { MetricsTableBase } from './MetricsTable.jsx';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import MetricsTable from './MetricsTable.jsx';
|
||||
import { routerWrap } from '../../test/testHelpers.jsx';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
describe('Tests for <MetricsTableBase>', () => {
|
||||
describe('Tests for <MetricsTable>', () => {
|
||||
const defaultProps = {
|
||||
api: ApiHelpers(''),
|
||||
};
|
||||
|
||||
it('renders the table with all columns', () => {
|
||||
const component = shallow(
|
||||
<MetricsTableBase
|
||||
{...defaultProps}
|
||||
metrics={[{
|
||||
name: 'web',
|
||||
namespace: 'default',
|
||||
totalRequests: 0,
|
||||
}]}
|
||||
resource="deployment" />
|
||||
);
|
||||
let extraProps = _.merge({}, defaultProps, {
|
||||
metrics: [{
|
||||
name: 'web',
|
||||
namespace: 'default',
|
||||
key: 'web-default-deploy',
|
||||
totalRequests: 0,
|
||||
}],
|
||||
resource: "deployment"
|
||||
});
|
||||
const component = mount(routerWrap(MetricsTable, extraProps));
|
||||
|
||||
const table = component.find(BaseTable);
|
||||
const table = component.find("BaseTable");
|
||||
|
||||
expect(table).toHaveLength(1);
|
||||
expect(table.props().dataSource).toHaveLength(1);
|
||||
expect(table.props().columns).toHaveLength(10);
|
||||
expect(table).toBeDefined();
|
||||
expect(table.props().tableRows).toHaveLength(1);
|
||||
expect(table.props().tableColumns).toHaveLength(10);
|
||||
});
|
||||
|
||||
it('omits the namespace column for the namespace resource', () => {
|
||||
const component = shallow(
|
||||
<MetricsTableBase
|
||||
{...defaultProps}
|
||||
metrics={[]}
|
||||
resource="namespace" />
|
||||
);
|
||||
let extraProps = _.merge({}, defaultProps, { metrics: [], resource: "namespace"});
|
||||
const component = mount(routerWrap(MetricsTable, extraProps));
|
||||
|
||||
const table = component.find(BaseTable);
|
||||
const table = component.find("BaseTable");
|
||||
|
||||
expect(table).toHaveLength(1);
|
||||
expect(table.props().columns).toHaveLength(9);
|
||||
expect(table).toBeDefined();
|
||||
expect(table.props().tableColumns).toHaveLength(9);
|
||||
});
|
||||
|
||||
it('omits the namespace column when showNamespaceColumn is false', () => {
|
||||
const component = shallow(
|
||||
<MetricsTableBase
|
||||
{...defaultProps}
|
||||
metrics={[]}
|
||||
resource="deployment"
|
||||
showNamespaceColumn={false} />
|
||||
);
|
||||
let extraProps = _.merge({}, defaultProps, {
|
||||
metrics: [],
|
||||
resource: "deployment",
|
||||
showNamespaceColumn: false
|
||||
});
|
||||
const component = mount(routerWrap(MetricsTable, extraProps));
|
||||
|
||||
const table = component.find(BaseTable);
|
||||
const table = component.find("BaseTable");
|
||||
|
||||
expect(table).toHaveLength(1);
|
||||
expect(table.props().columns).toHaveLength(9);
|
||||
expect(table).toBeDefined();
|
||||
expect(table.props().tableColumns).toHaveLength(9);
|
||||
});
|
||||
|
||||
it('omits meshed column and grafana column for authority resource', () => {
|
||||
const component = shallow(
|
||||
<MetricsTableBase
|
||||
{...defaultProps}
|
||||
metrics={[]}
|
||||
resource="authority" />
|
||||
);
|
||||
let extraProps = _.merge({}, defaultProps, { metrics: [], resource: "authority"});
|
||||
const component = mount(routerWrap(MetricsTable, extraProps));
|
||||
|
||||
const table = component.find(BaseTable);
|
||||
const table = component.find("BaseTable");
|
||||
|
||||
expect(table).toHaveLength(1);
|
||||
expect(table.props().columns).toHaveLength(8);
|
||||
expect(table).toBeDefined();
|
||||
expect(table.props().tableColumns).toHaveLength(8);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import _ from 'lodash';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import { friendlyTitle } from './util/Utils.js';
|
||||
import MetricsTable from './MetricsTable.jsx';
|
||||
import NetworkGraph from './NetworkGraph.jsx';
|
||||
import { processMultiResourceRollup } from './util/MetricUtils.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Spin } from 'antd';
|
||||
import Spinner from './util/Spinner.jsx';
|
||||
import _ from 'lodash';
|
||||
import { friendlyTitle } from './util/Utils.js';
|
||||
import { processMultiResourceRollup } from './util/MetricUtils.jsx';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
class Namespaces extends React.Component {
|
||||
static defaultProps = {
|
||||
|
@ -60,12 +61,6 @@ class Namespaces extends React.Component {
|
|||
this.timerId = window.setInterval(this.loadFromServer, this.state.pollingInterval);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
// React won't unmount this component when switching resource pages so we need to clear state
|
||||
this.api.cancelCurrentRequests();
|
||||
this.setState(this.getInitialState(newProps.match.params));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.clearInterval(this.timerId);
|
||||
this.api.cancelCurrentRequests();
|
||||
|
@ -127,7 +122,7 @@ class Namespaces extends React.Component {
|
|||
return (
|
||||
<div className="page-content">
|
||||
{ !this.state.error ? null : <ErrorBanner message={this.state.error} /> }
|
||||
{ !this.state.loaded ? <Spin size="large" /> : (
|
||||
{ !this.state.loaded ? <Spinner /> : (
|
||||
<div>
|
||||
{ noMetrics ? <div>No resources detected.</div> : null}
|
||||
{
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
import _ from 'lodash';
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import { friendlyTitle } from './util/Utils.js';
|
||||
import MetricsTable from './MetricsTable.jsx';
|
||||
import PageHeader from './PageHeader.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { Collapse, Icon, Spin, Tooltip } from 'antd';
|
||||
import { processMultiResourceRollup, processSingleResourceRollup } from './util/MetricUtils.jsx';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
import { processMultiResourceRollup, processSingleResourceRollup } from './util/MetricUtils.jsx';
|
||||
|
||||
import Accordion from './util/Accordion.jsx';
|
||||
import CheckCircleIcon from '@material-ui/icons/CheckCircle';
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import MetricsTable from './MetricsTable.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Spinner from './util/Spinner.jsx';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import _ from 'lodash';
|
||||
import { friendlyTitle } from './util/Utils.js';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
|
||||
const isMeshedTooltip = (
|
||||
<Tooltip placement="right" title="Namespace is meshed" overlayStyle={{ fontSize: "12px" }}>
|
||||
<Icon className="status-ok" type="check-circle" />
|
||||
<Tooltip title="Namespace is meshed" placement="right-start">
|
||||
<CheckCircleIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
class NamespaceLanding extends React.Component {
|
||||
|
@ -57,7 +62,7 @@ class NamespaceLanding extends React.Component {
|
|||
this.api.cancelCurrentRequests();
|
||||
}
|
||||
|
||||
onNsSelect = ns => {
|
||||
onNamespaceChange = ns => {
|
||||
this.setState({
|
||||
selectedNs: ns
|
||||
});
|
||||
|
@ -120,7 +125,8 @@ class NamespaceLanding extends React.Component {
|
|||
}
|
||||
return (
|
||||
<div className="page-section">
|
||||
<h3>{friendlyTitle(resource).plural}</h3>
|
||||
<br />
|
||||
<Typography variant="h5">{friendlyTitle(resource).plural}</Typography>
|
||||
<MetricsTable
|
||||
resource={resource}
|
||||
metrics={metrics}
|
||||
|
@ -131,7 +137,7 @@ class NamespaceLanding extends React.Component {
|
|||
|
||||
renderNamespaceSection(namespace) {
|
||||
if (!_.has(this.state.metricsByNs, namespace)) {
|
||||
return <Spin size="large" className="short-spin-section" />;
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
let metrics = this.state.metricsByNs[namespace] || {};
|
||||
|
@ -139,7 +145,7 @@ class NamespaceLanding extends React.Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h2>Namespace: {namespace}</h2>
|
||||
<Typography variant="h4">Namespace: {namespace}</Typography>
|
||||
{ noMetrics ? <div>No resources detected.</div> : null}
|
||||
{this.renderResourceSection("deployment", metrics.deployment)}
|
||||
{this.renderResourceSection("replicationcontroller", metrics.replicationcontroller)}
|
||||
|
@ -150,31 +156,21 @@ class NamespaceLanding extends React.Component {
|
|||
}
|
||||
|
||||
renderAccordion() {
|
||||
let panelData = _.map(this.state.namespaces, ns => {
|
||||
return {
|
||||
id: ns.name,
|
||||
header: <React.Fragment>{ns.name} {!ns.added ? null : isMeshedTooltip}</React.Fragment>,
|
||||
body: ns.name === this.state.selectedNs || ns.name === this.state.defaultOpenNs.name ?
|
||||
this.renderNamespaceSection(ns.name) : null
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PageHeader header="Namespaces" />
|
||||
<Collapse
|
||||
accordion={true}
|
||||
defaultActiveKey={_.get(this.state.defaultOpenNs, 'name', null)}
|
||||
onChange={this.onNsSelect}>
|
||||
{
|
||||
_.map(this.state.namespaces, ns => {
|
||||
let header = (
|
||||
<React.Fragment>{ns.name} {!ns.added ? null : isMeshedTooltip}</React.Fragment>
|
||||
);
|
||||
|
||||
return (
|
||||
<Collapse.Panel
|
||||
showArrow={false}
|
||||
header={header}
|
||||
key={ns.name}>
|
||||
{ns.name === this.state.selectedNs || ns.name === this.state.defaultOpenNs.name ?
|
||||
this.renderNamespaceSection(ns.name) : null}
|
||||
</Collapse.Panel>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Collapse>
|
||||
<Accordion
|
||||
onChange={this.onNamespaceChange}
|
||||
panels={panelData}
|
||||
defaultOpenPanel={_.get(this.state.defaultOpenNs, 'name', null)} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -183,7 +179,7 @@ class NamespaceLanding extends React.Component {
|
|||
return (
|
||||
<div className="page-content">
|
||||
{ !this.state.error ? null : <ErrorBanner message={this.state.error} /> }
|
||||
{ !this.state.loaded ? <Spin size="large" /> : this.renderAccordion() }
|
||||
{ !this.state.loaded ? <Spinner /> : this.renderAccordion() }
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,329 @@
|
|||
import {
|
||||
AppBar,
|
||||
Collapse,
|
||||
Divider,
|
||||
Drawer,
|
||||
IconButton,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Toolbar,
|
||||
Typography
|
||||
} from '@material-ui/core';
|
||||
import { githubIcon, linkerdWordLogo, slackIcon } from './util/SvgWrappers.jsx';
|
||||
|
||||
import BreadcrumbHeader from './BreadcrumbHeader.jsx';
|
||||
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft';
|
||||
import CloudQueueIcon from '@material-ui/icons/CloudQueue';
|
||||
import EmailIcon from '@material-ui/icons/Email';
|
||||
import ExpandLess from '@material-ui/icons/ExpandLess';
|
||||
import ExpandMore from '@material-ui/icons/ExpandMore';
|
||||
import HelpIcon from '@material-ui/icons/HelpOutline';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import LibraryBooksIcon from '@material-ui/icons/LibraryBooks';
|
||||
import { Link } from 'react-router-dom';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import NavigateNextIcon from '@material-ui/icons/NavigateNext';
|
||||
import NetworkCheckIcon from '@material-ui/icons/NetworkCheck';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ReactRouterPropTypes from 'react-router-prop-types';
|
||||
import Version from './Version.jsx';
|
||||
import ViewListIcon from '@material-ui/icons/ViewList';
|
||||
import VisibilityIcon from '@material-ui/icons/Visibility';
|
||||
import classNames from 'classnames';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
const drawerWidth = 250;
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
height: "100vh",
|
||||
width: "100%",
|
||||
zIndex: 1,
|
||||
overflowX: 'scroll',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
},
|
||||
appBar: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
transition: theme.transitions.create(['width', 'margin'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
},
|
||||
appBarShift: {
|
||||
marginLeft: drawerWidth,
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
transition: theme.transitions.create(['width', 'margin'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
menuButton: {
|
||||
marginLeft: 12,
|
||||
marginRight: 36,
|
||||
},
|
||||
hide: {
|
||||
display: 'none',
|
||||
},
|
||||
drawerPaper: {
|
||||
position: 'relative',
|
||||
whiteSpace: 'nowrap',
|
||||
width: drawerWidth,
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
},
|
||||
drawerPaperClose: {
|
||||
overflowX: 'hidden',
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
width: theme.spacing.unit * 7,
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: theme.spacing.unit * 9,
|
||||
},
|
||||
},
|
||||
toolbar: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
padding: '0 8px',
|
||||
...theme.mixins.toolbar,
|
||||
},
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
padding: theme.spacing.unit * 3,
|
||||
},
|
||||
linkerdLogoContainer: {
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
},
|
||||
linkerdNavLogo: {
|
||||
minWidth: "180px",
|
||||
},
|
||||
navMenuItem: {
|
||||
paddingLeft: "24px",
|
||||
paddingRight: "24px",
|
||||
},
|
||||
});
|
||||
|
||||
class NavigationBase extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = this.props.api;
|
||||
this.handleApiError = this.handleApiError.bind(this);
|
||||
|
||||
this.state = this.getInitialState();
|
||||
}
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
drawerOpen: true,
|
||||
resourceMenuOpen: false,
|
||||
helpMenuOpen: false,
|
||||
latestVersion: '',
|
||||
isLatest: true,
|
||||
namespaceFilter: "all"
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchVersion();
|
||||
}
|
||||
|
||||
fetchVersion() {
|
||||
let versionUrl = `https://versioncheck.linkerd.io/version.json?version=${this.props.releaseVersion}&uuid=${this.props.uuid}&source=web`;
|
||||
this.versionPromise = fetch(versionUrl, { credentials: 'include' })
|
||||
.then(rsp => rsp.json())
|
||||
.then(versionRsp => {
|
||||
let latestVersion;
|
||||
let parts = this.props.releaseVersion.split("-", 2);
|
||||
if (parts.length === 2) {
|
||||
latestVersion = versionRsp[parts[0]];
|
||||
}
|
||||
this.setState({
|
||||
latestVersion,
|
||||
isLatest: latestVersion === this.props.releaseVersion
|
||||
});
|
||||
}).catch(this.handleApiError);
|
||||
}
|
||||
|
||||
handleApiError(e) {
|
||||
this.setState({
|
||||
error: e
|
||||
});
|
||||
}
|
||||
|
||||
handleDrawerOpen = () => {
|
||||
this.setState({ drawerOpen: true });
|
||||
};
|
||||
|
||||
handleDrawerClose = () => {
|
||||
this.setState({ drawerOpen: false });
|
||||
};
|
||||
|
||||
handleResourceMenuClick = () => {
|
||||
this.setState(state => ({ resourceMenuOpen: !state.resourceMenuOpen }));
|
||||
};
|
||||
|
||||
handleHelpMenuClick = () => {
|
||||
this.setState(state => ({ helpMenuOpen: !state.helpMenuOpen }));
|
||||
}
|
||||
|
||||
menuItem(path, title, icon) {
|
||||
const { classes, api } = this.props;
|
||||
let normalizedPath = this.props.location.pathname.replace(this.props.pathPrefix, "");
|
||||
let isCurrentPage = path => path === normalizedPath;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={api.prefixLink(path)}
|
||||
className={classes.navMenuItem}
|
||||
selected={isCurrentPage(path)}>
|
||||
<ListItemIcon>{icon}</ListItemIcon>
|
||||
<ListItemText primary={title} />
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
const { classes, ChildComponent } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<AppBar
|
||||
position="absolute"
|
||||
className={classNames(classes.appBar, {[classes.appBarShift]: this.state.drawerOpen} )}>
|
||||
<Toolbar disableGutters={!this.state.drawerOpen}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="Open drawer"
|
||||
onClick={this.handleDrawerOpen}
|
||||
className={classNames(classes.menuButton, {[classes.hide]: this.state.drawerOpen} )}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
|
||||
<Typography variant="h6" color="inherit" noWrap>
|
||||
<BreadcrumbHeader {...this.props} />
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
classes={{
|
||||
paper: classNames(classes.drawerPaper, {[classes.drawerPaperClose]: !this.state.drawerOpen} ),
|
||||
}}
|
||||
open={this.state.drawerOpen}>
|
||||
<div className={classNames(classes.linkerdLogoContainer, classes.toolbar)}>
|
||||
<div className={classes.linkerdNavLogo}>
|
||||
{linkerdWordLogo}
|
||||
</div>
|
||||
<IconButton className="drawer-toggle-btn" onClick={this.handleDrawerClose}>
|
||||
<ChevronLeftIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<MenuList>
|
||||
{ this.menuItem("/overview", "Overview", <HomeIcon />) }
|
||||
{ this.menuItem("/tap", "Tap", <VisibilityIcon />) }
|
||||
{ this.menuItem("/top", "Top", <NetworkCheckIcon />) }
|
||||
{ this.menuItem("/servicemesh", "Service Mesh", <CloudQueueIcon />) }
|
||||
<MenuItem
|
||||
className={classes.navMenuItem}
|
||||
button
|
||||
onClick={this.handleResourceMenuClick}>
|
||||
<ListItemIcon><ViewListIcon /></ListItemIcon>
|
||||
<ListItemText inset primary="Resources" />
|
||||
{this.state.resourceMenuOpen ? <ExpandLess /> : <ExpandMore />}
|
||||
</MenuItem>
|
||||
|
||||
<Collapse in={this.state.resourceMenuOpen} timeout="auto" unmountOnExit>
|
||||
<MenuList dense component="div" disablePadding>
|
||||
{ this.menuItem("/authorities", "Authorities", <NavigateNextIcon />) }
|
||||
{ this.menuItem("/deployments", "Deployments", <NavigateNextIcon />) }
|
||||
{ this.menuItem("/namespaces", "Namespaces", <NavigateNextIcon />) }
|
||||
{ this.menuItem("/pods", "Pods", <NavigateNextIcon />) }
|
||||
{ this.menuItem("/replicationcontrollers", "Replication Controllers", <NavigateNextIcon />) }
|
||||
</MenuList>
|
||||
</Collapse>
|
||||
</MenuList>
|
||||
|
||||
<Divider />
|
||||
|
||||
<MenuList>
|
||||
<ListItem component="a" href="https://linkerd.io/2/overview/" target="_blank">
|
||||
<ListItemIcon><LibraryBooksIcon /></ListItemIcon>
|
||||
<ListItemText primary="Documentation" />
|
||||
</ListItem>
|
||||
|
||||
<MenuItem
|
||||
className={classes.navMenuItem}
|
||||
button
|
||||
onClick={this.handleHelpMenuClick}>
|
||||
<ListItemIcon><HelpIcon /></ListItemIcon>
|
||||
<ListItemText inset primary="Help" />
|
||||
{this.state.helpMenuOpen ? <ExpandLess /> : <ExpandMore />}
|
||||
</MenuItem>
|
||||
<Collapse in={this.state.helpMenuOpen} timeout="auto" unmountOnExit>
|
||||
<MenuList dense component="div" disablePadding>
|
||||
<ListItem component="a" href="https://lists.cncf.io/g/cncf-linkerd-users" target="_blank">
|
||||
<ListItemIcon><EmailIcon /></ListItemIcon>
|
||||
<ListItemText primary="Join the mailing list" />
|
||||
</ListItem>
|
||||
|
||||
<ListItem component="a" href="https://slack.linkerd.io" target="_blank">
|
||||
<ListItemIcon>{slackIcon}</ListItemIcon>
|
||||
<ListItemText primary="Join us on slack" />
|
||||
</ListItem>
|
||||
|
||||
<ListItem component="a" href="https://github.com/linkerd/linkerd2/issues/new/choose" target="_blank">
|
||||
<ListItemIcon>{githubIcon}</ListItemIcon>
|
||||
<ListItemText primary="File an issue" />
|
||||
</ListItem>
|
||||
</MenuList>
|
||||
</Collapse>
|
||||
</MenuList>
|
||||
|
||||
{
|
||||
!this.state.drawerOpen ? null : <Version
|
||||
isLatest={this.state.isLatest}
|
||||
latestVersion={this.state.latestVersion}
|
||||
releaseVersion={this.props.releaseVersion}
|
||||
error={this.state.error}
|
||||
uuid={this.props.uuid} />
|
||||
}
|
||||
</Drawer>
|
||||
<main className={classes.content}>
|
||||
<div className={classes.toolbar} />
|
||||
<div className="main-content"><ChildComponent {...this.props} /></div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NavigationBase.propTypes = {
|
||||
api: PropTypes.shape({
|
||||
PrefixedLink: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
ChildComponent: PropTypes.func.isRequired,
|
||||
classes: PropTypes.shape({}).isRequired,
|
||||
location: ReactRouterPropTypes.location.isRequired,
|
||||
pathPrefix: PropTypes.string.isRequired,
|
||||
releaseVersion: PropTypes.string.isRequired,
|
||||
theme: PropTypes.shape({}).isRequired,
|
||||
uuid: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default withContext(withStyles(styles, { withTheme: true })(NavigationBase));
|
|
@ -1,7 +1,7 @@
|
|||
import ApiHelpers from './util/ApiHelpers.jsx';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import Sidebar from './Sidebar.jsx';
|
||||
import Navigation from './Navigation.jsx';
|
||||
import sinon from 'sinon';
|
||||
import sinonStubPromise from 'sinon-stub-promise';
|
||||
import { mount } from 'enzyme';
|
||||
|
@ -15,15 +15,17 @@ const loc = {
|
|||
search: '',
|
||||
};
|
||||
|
||||
describe('Version', () => {
|
||||
|
||||
describe('Navigation', () => {
|
||||
let curVer = "edge-1.2.3";
|
||||
let newVer = "edge-2.3.4";
|
||||
|
||||
let component, fetchStub;
|
||||
let apiHelpers = ApiHelpers("");
|
||||
const childComponent = () => null;
|
||||
|
||||
function withPromise(fn) {
|
||||
return component.find("Sidebar").instance().serverPromise.then(fn);
|
||||
return component.find("NavigationBase").instance().versionPromise.then(fn);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -37,7 +39,7 @@ describe('Version', () => {
|
|||
|
||||
const expandSidebar = component => {
|
||||
// click trigger to expand the sidebar
|
||||
component.find(".ant-layout-sider-trigger").simulate('click');
|
||||
component.find("button.drawer-toggle-btn").simulate('click', () => {});
|
||||
};
|
||||
|
||||
it('is hidden when the sidebar is collapsed', () => {
|
||||
|
@ -48,7 +50,10 @@ describe('Version', () => {
|
|||
|
||||
component = mount(
|
||||
<BrowserRouter>
|
||||
<Sidebar
|
||||
<Navigation
|
||||
ChildComponent={childComponent}
|
||||
classes={{}}
|
||||
theme={{}}
|
||||
location={loc}
|
||||
api={apiHelpers}
|
||||
releaseVersion={curVer}
|
||||
|
@ -72,7 +77,10 @@ describe('Version', () => {
|
|||
|
||||
component = mount(
|
||||
<BrowserRouter>
|
||||
<Sidebar
|
||||
<Navigation
|
||||
ChildComponent={childComponent}
|
||||
classes={{}}
|
||||
theme={{}}
|
||||
location={loc}
|
||||
api={apiHelpers}
|
||||
releaseVersion={curVer}
|
||||
|
@ -94,7 +102,10 @@ describe('Version', () => {
|
|||
|
||||
component = mount(
|
||||
<BrowserRouter>
|
||||
<Sidebar
|
||||
<Navigation
|
||||
ChildComponent={childComponent}
|
||||
classes={{}}
|
||||
theme={{}}
|
||||
location={loc}
|
||||
api={apiHelpers}
|
||||
releaseVersion={curVer}
|
||||
|
@ -111,17 +122,20 @@ describe('Version', () => {
|
|||
it('renders error when version check fails', () => {
|
||||
let errMsg = "Fake error";
|
||||
|
||||
fetchStub.resolves({
|
||||
fetchStub.rejects({
|
||||
ok: false,
|
||||
json: () => Promise.resolve({
|
||||
error: errMsg
|
||||
error: {},
|
||||
}),
|
||||
statusText: errMsg,
|
||||
});
|
||||
|
||||
component = mount(
|
||||
<BrowserRouter>
|
||||
<Sidebar
|
||||
<Navigation
|
||||
ChildComponent={childComponent}
|
||||
classes={{}}
|
||||
theme={{}}
|
||||
location={loc}
|
||||
api={apiHelpers}
|
||||
releaseVersion={curVer}
|
|
@ -1,12 +1,13 @@
|
|||
import _ from 'lodash';
|
||||
import { metricsPropType } from './util/MetricUtils.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import withREST from './util/withREST.jsx';
|
||||
import * as d3 from 'd3';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
import * as d3 from 'd3';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import { metricsPropType } from './util/MetricUtils.jsx';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import withREST from './util/withREST.jsx';
|
||||
|
||||
const defaultSvgWidth = 524;
|
||||
const defaultSvgHeight = 325;
|
||||
|
|
|
@ -1,28 +1,22 @@
|
|||
import _ from 'lodash';
|
||||
import { displayName, metricToFormatter } from './util/Utils.js';
|
||||
import { getSuccessRateClassification, srArcClassLabels } from './util/MetricUtils.jsx' ;
|
||||
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import OctopusArms from './util/OctopusArms.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Col, Progress, Row } from 'antd';
|
||||
import { displayName, metricToFormatter } from './util/Utils.js';
|
||||
import { getSuccessRateClassification, srArcClassLabels } from './util/MetricUtils.jsx' ;
|
||||
import './../../css/octopus.css';
|
||||
import { StyledProgress } from './util/Progress.jsx';
|
||||
import Table from '@material-ui/core/Table';
|
||||
import TableBody from '@material-ui/core/TableBody';
|
||||
import TableCell from '@material-ui/core/TableCell';
|
||||
import TableRow from '@material-ui/core/TableRow';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import _ from 'lodash';
|
||||
|
||||
const maxNumNeighbors = 6; // max number of neighbor nodes to show in the octopus graph
|
||||
|
||||
const Metric = ({title, value, className}) => {
|
||||
return (
|
||||
<Row type="flex" justify="center" className={`octopus-metric ${className}`}>
|
||||
<Col span={12} className="octopus-metric-title"><div>{title}</div></Col>
|
||||
<Col span={12} className="octopus-metric-value"><div>{value}</div></Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
Metric.defaultProps = { className: "" };
|
||||
Metric.propTypes = {
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default class Octopus extends React.Component {
|
||||
static defaultProps = {
|
||||
|
@ -74,54 +68,68 @@ export default class Octopus extends React.Component {
|
|||
linkText={display} />;
|
||||
}
|
||||
|
||||
renderResourceSummary(resource, type) {
|
||||
renderResourceCard(resource, type) {
|
||||
let display = displayName(resource);
|
||||
let classification = getSuccessRateClassification(resource.successRate, srArcClassLabels);
|
||||
let Progress = StyledProgress(classification);
|
||||
|
||||
return (
|
||||
<div key={resource.name} className={`octopus-body ${type}`} title={display}>
|
||||
<div className={`octopus-title ${type}-title`}>
|
||||
{ this.linkedResourceTitle(resource, display) }
|
||||
</div>
|
||||
<div>
|
||||
<div className="octopus-sr-gauge">
|
||||
<Progress
|
||||
className={`success-rate-arc ${getSuccessRateClassification(resource.successRate, srArcClassLabels)}`}
|
||||
type="dashboard"
|
||||
format={() => metricToFormatter["SUCCESS_RATE"](resource.successRate)}
|
||||
width={type === "main" ? 132 : 64}
|
||||
percent={resource.successRate * 100}
|
||||
gapDegree={180} />
|
||||
</div>
|
||||
<Metric title="RPS" value={metricToFormatter["REQUEST_RATE"](resource.requestRate)} />
|
||||
<Metric title="P99" value={metricToFormatter["LATENCY"](_.get(resource, "latency.P99"))} />
|
||||
</div>
|
||||
</div>
|
||||
<Grid item key={resource.name} >
|
||||
<Card className={`octopus-body ${type}`} title={display}>
|
||||
<CardContent>
|
||||
|
||||
<Typography variant={type === "neighbor" ? "h6" : "h4"} align="center">
|
||||
{ this.linkedResourceTitle(resource, display) }
|
||||
</Typography>
|
||||
|
||||
<Progress variant="determinate" value={resource.successRate * 100} />
|
||||
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell><Typography>RPS</Typography></TableCell>
|
||||
<TableCell numeric={true}><Typography>{metricToFormatter["NO_UNIT"](resource.requestRate)}</Typography></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell><Typography>P99</Typography></TableCell>
|
||||
<TableCell numeric={true}><Typography>{metricToFormatter["LATENCY"](_.get(resource, "latency.P99"))}</Typography></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
renderUnmeshedResources = unmeshedResources => {
|
||||
return (
|
||||
<div key="unmeshed-resources" className="octopus-body neighbor unmeshed">
|
||||
<div className="octopus-title neighbor-title">Unmeshed</div>
|
||||
{
|
||||
_.map(unmeshedResources, r => {
|
||||
let display = displayName(r);
|
||||
return <div key={display} title={display}>{display}</div>;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<Grid item>
|
||||
<Card key="unmeshed-resources">
|
||||
<CardContent>
|
||||
<Typography variant="h6">Unmeshed</Typography>
|
||||
{
|
||||
_.map(unmeshedResources, r => {
|
||||
let display = displayName(r);
|
||||
return <Typography key={display} variant="body2" title={display}>{display}</Typography>;
|
||||
})
|
||||
}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
renderCollapsedNeighbors = neighbors => {
|
||||
return (
|
||||
<div key="unmeshed-resources" className="octopus-body neighbor collapsed">
|
||||
<Grid item key="unmeshed-resources">
|
||||
{
|
||||
_.map(neighbors, r => {
|
||||
let display = displayName(r);
|
||||
return <div className="octopus-title neighbor-title" key={display}>{this.linkedResourceTitle(r, display)}</div>;
|
||||
return <div key={display}>{this.linkedResourceTitle(r, display)}</div>;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -177,30 +185,51 @@ export default class Octopus extends React.Component {
|
|||
|
||||
return (
|
||||
<div className="octopus-graph">
|
||||
<Row type="flex" justify="center" align="middle">
|
||||
<Col span={6} className={`octopus-col ${hasUpstreams ? "resource-col" : ""}`}>
|
||||
{_.map(display.upstreams.displayed, n => this.renderResourceSummary(n, "neighbor"))}
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justify="center"
|
||||
alignItems="center">
|
||||
|
||||
<Grid
|
||||
container
|
||||
spacing={24}
|
||||
direction="column"
|
||||
justify="center"
|
||||
alignItems="center"
|
||||
item
|
||||
xs={3}
|
||||
className={`octopus-col ${hasUpstreams ? "resource-col" : ""}`}>
|
||||
{_.map(display.upstreams.displayed, n => this.renderResourceCard(n, "neighbor"))}
|
||||
{_.isEmpty(unmeshedSources) ? null : this.renderUnmeshedResources(unmeshedSources)}
|
||||
{_.isEmpty(display.upstreams.collapsed) ? null : this.renderCollapsedNeighbors(display.upstreams.collapsed)}
|
||||
</Col>
|
||||
</Grid>
|
||||
|
||||
<Col span={2} className="octopus-col">
|
||||
<Grid item xs={1} className="octopus-col">
|
||||
{this.renderArrowCol(numUpstreams, false)}
|
||||
</Col>
|
||||
</Grid>
|
||||
|
||||
<Col span={8} className="octopus-col resource-col">
|
||||
{this.renderResourceSummary(resource, "main")}
|
||||
</Col>
|
||||
<Grid item xs={4} className="octopus-col resource-col">
|
||||
{this.renderResourceCard(resource, "main")}
|
||||
</Grid>
|
||||
|
||||
<Col span={2} className="octopus-col">
|
||||
<Grid item xs={1} className="octopus-col">
|
||||
{this.renderArrowCol(numDownstreams, true)}
|
||||
</Col>
|
||||
</Grid>
|
||||
|
||||
<Col span={6} className={`octopus-col ${hasDownstreams ? "resource-col" : ""}`}>
|
||||
{_.map(display.downstreams.displayed, n => this.renderResourceSummary(n, "neighbor"))}
|
||||
<Grid
|
||||
container
|
||||
spacing={24}
|
||||
direction="column"
|
||||
justify="center"
|
||||
alignItems="center"
|
||||
item
|
||||
xs={3}
|
||||
className={`octopus-col ${hasDownstreams ? "resource-col" : ""}`}>
|
||||
{_.map(display.downstreams.displayed, n => this.renderResourceCard(n, "neighbor"))}
|
||||
{_.isEmpty(display.downstreams.collapsed) ? null : this.renderCollapsedNeighbors(display.downstreams.collapsed)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { Col, Radio, Row } from 'antd';
|
||||
|
||||
class PageHeader extends React.Component {
|
||||
static defaultProps = {
|
||||
hideButtons: false,
|
||||
subHeader: null,
|
||||
subHeaderTitle: null,
|
||||
subMessage: null,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
api: PropTypes.shape({
|
||||
getMetricsWindow: PropTypes.func.isRequired,
|
||||
getValidMetricsWindows: PropTypes.func.isRequired,
|
||||
setMetricsWindow: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
header: PropTypes.string.isRequired,
|
||||
hideButtons: PropTypes.bool,
|
||||
subHeader: PropTypes.string,
|
||||
subHeaderTitle: PropTypes.string,
|
||||
subMessage: PropTypes.string,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onTimeWindowClick = this.onTimeWindowClick.bind(this);
|
||||
this.state = {
|
||||
selectedWindow: this.props.api.getMetricsWindow()
|
||||
};
|
||||
}
|
||||
|
||||
onTimeWindowClick(e) {
|
||||
let window = e.target.value;
|
||||
this.props.api.setMetricsWindow(window);
|
||||
this.setState({selectedWindow: window});
|
||||
}
|
||||
|
||||
// varying the time window on the web UI is currently hidden
|
||||
renderMetricWindowButtons() {
|
||||
if (this.props.hideButtons) {return null;}
|
||||
|
||||
const buttons = _.map(this.props.api.getValidMetricsWindows(), (w, i) => {
|
||||
return <Radio.Button key={`metrics-window-btn-${i}`} value={w}>{w}</Radio.Button>;
|
||||
});
|
||||
|
||||
return (
|
||||
<Col span={6}>
|
||||
<Radio.Group
|
||||
className="time-window-btns"
|
||||
value={this.state.selectedWindow}
|
||||
onChange={this.onTimeWindowClick} >
|
||||
{buttons}
|
||||
</Radio.Group>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {header, subHeader, subHeaderTitle, subMessage} = this.props;
|
||||
|
||||
return (
|
||||
<div className="page-header">
|
||||
<Row>
|
||||
<Col span={18}>
|
||||
{!header ? null : <h1>{header}</h1>}
|
||||
{!subHeaderTitle ? null : <div className="subsection-header">{subHeaderTitle}</div>}
|
||||
{!subHeader ? null : <h1>{subHeader}</h1>}
|
||||
</Col>
|
||||
{/* {this.renderMetricWindowButtons()} */}
|
||||
</Row>
|
||||
|
||||
{!subMessage ? null : <div>{subMessage}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withContext(PageHeader);
|
|
@ -0,0 +1,74 @@
|
|||
/* eslint-disable jsx-a11y/mouse-events-have-key-events */
|
||||
import Popover from '@material-ui/core/Popover';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
const styles = theme => ({
|
||||
popover: {
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
paper: {
|
||||
padding: theme.spacing.unit,
|
||||
},
|
||||
});
|
||||
|
||||
class MouseOverPopover extends React.Component {
|
||||
state = {
|
||||
anchorEl: null,
|
||||
};
|
||||
|
||||
handlePopoverOpen = event => {
|
||||
this.setState({ anchorEl: event.currentTarget });
|
||||
};
|
||||
|
||||
handlePopoverClose = () => {
|
||||
this.setState({ anchorEl: null });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes, baseContent, popoverContent } = this.props;
|
||||
const { anchorEl } = this.state;
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
aria-owns={open ? 'mouse-over-popover' : null}
|
||||
aria-haspopup="true"
|
||||
onMouseEnter={this.handlePopoverOpen}
|
||||
onMouseLeave={this.handlePopoverClose}>
|
||||
{baseContent}
|
||||
</div>
|
||||
<Popover
|
||||
id="mouse-over-popover"
|
||||
className={classes.popover}
|
||||
classes={{
|
||||
paper: classes.paper,
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
onClose={this.handlePopoverClose}
|
||||
disableRestoreFocus>
|
||||
{popoverContent}
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MouseOverPopover.propTypes = {
|
||||
baseContent: PropTypes.node.isRequired,
|
||||
classes: PropTypes.shape({}).isRequired,
|
||||
popoverContent: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
export default withStyles(styles)(MouseOverPopover);
|
|
@ -1,17 +1,19 @@
|
|||
import _ from 'lodash';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
import { emptyMetric, processSingleResourceRollup } from './util/MetricUtils.jsx';
|
||||
import { resourceTypeToCamelCase, singularResource } from './util/Utils.js';
|
||||
|
||||
import AddResources from './AddResources.jsx';
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import MetricsTable from './MetricsTable.jsx';
|
||||
import Octopus from './Octopus.jsx';
|
||||
import { processNeighborData } from './util/TapUtils.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Spin } from 'antd';
|
||||
import Spinner from './util/Spinner.jsx';
|
||||
import TopModule from './TopModule.jsx';
|
||||
import _ from 'lodash';
|
||||
import { processNeighborData } from './util/TapUtils.jsx';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { emptyMetric, processSingleResourceRollup } from './util/MetricUtils.jsx';
|
||||
import { resourceTypeToCamelCase, singularResource } from './util/Utils.js';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
const getResourceFromUrl = (match, pathPrefix) => {
|
||||
let resource = {
|
||||
|
@ -34,7 +36,9 @@ export class ResourceDetailBase extends React.Component {
|
|||
api: PropTypes.shape({
|
||||
PrefixedLink: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
match: PropTypes.shape({}).isRequired,
|
||||
match: PropTypes.shape({
|
||||
url: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
pathPrefix: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
|
@ -74,11 +78,13 @@ export class ResourceDetailBase extends React.Component {
|
|||
this.timerId = window.setInterval(this.loadFromServer, this.state.pollingInterval);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
// React won't unmount this component when switching resource pages so we need to clear state
|
||||
this.api.cancelCurrentRequests();
|
||||
this.unmeshedSources = {};
|
||||
this.setState(this.getInitialState(newProps.match, newProps.pathPrefix));
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!_.isEqual(prevProps.match.url, this.props.match.url)) {
|
||||
// React won't unmount this component when switching resource pages so we need to clear state
|
||||
this.api.cancelCurrentRequests();
|
||||
this.unmeshedSources = {};
|
||||
this.setState(this.getInitialState(this.props.match, this.props.pathPrefix));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -185,7 +191,7 @@ export class ResourceDetailBase extends React.Component {
|
|||
|
||||
content = () => {
|
||||
if (!this.state.loaded && !this.state.error) {
|
||||
return <Spin size="large" />;
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
let topQuery = {
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import _ from 'lodash';
|
||||
import { apiErrorPropType } from './util/ApiHelpers.jsx';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
import { metricsPropType, processSingleResourceRollup } from './util/MetricUtils.jsx';
|
||||
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import MetricsTable from './MetricsTable.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Spin } from 'antd';
|
||||
import Spinner from './util/Spinner.jsx';
|
||||
import _ from 'lodash';
|
||||
import { apiErrorPropType } from './util/ApiHelpers.jsx';
|
||||
import withREST from './util/withREST.jsx';
|
||||
import { metricsPropType, processSingleResourceRollup } from './util/MetricUtils.jsx';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
export class ResourceListBase extends React.Component {
|
||||
static defaultProps = {
|
||||
|
@ -35,7 +37,7 @@ export class ResourceListBase extends React.Component {
|
|||
const {data, loading, error} = this.props;
|
||||
|
||||
if (loading && !error) {
|
||||
return <Spin size="large" />;
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
let processedMetrics = [];
|
||||
|
@ -44,9 +46,11 @@ export class ResourceListBase extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
resource={this.props.resource}
|
||||
metrics={processedMetrics} />
|
||||
<React.Fragment>
|
||||
<MetricsTable
|
||||
resource={this.props.resource}
|
||||
metrics={processedMetrics} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import ErrorBanner from './ErrorBanner.jsx';
|
|||
import MetricsTable from './MetricsTable.jsx';
|
||||
import React from 'react';
|
||||
import { ResourceListBase } from './ResourceList.jsx';
|
||||
import { Spin } from 'antd';
|
||||
import Spinner from './util/Spinner.jsx';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
describe('Tests for <ResourceListBase>', () => {
|
||||
|
@ -24,7 +24,7 @@ describe('Tests for <ResourceListBase>', () => {
|
|||
|
||||
const err = component.find(ErrorBanner);
|
||||
expect(err).toHaveLength(1);
|
||||
expect(component.find(Spin)).toHaveLength(0);
|
||||
expect(component.find(Spinner)).toHaveLength(0);
|
||||
expect(component.find(MetricsTable)).toHaveLength(1);
|
||||
expect(err.props().message.statusText).toEqual(msg);
|
||||
});
|
||||
|
@ -38,7 +38,7 @@ describe('Tests for <ResourceListBase>', () => {
|
|||
);
|
||||
|
||||
expect(component.find(ErrorBanner)).toHaveLength(0);
|
||||
expect(component.find(Spin)).toHaveLength(1);
|
||||
expect(component.find(Spinner)).toHaveLength(1);
|
||||
expect(component.find(MetricsTable)).toHaveLength(0);
|
||||
});
|
||||
|
||||
|
@ -51,7 +51,7 @@ describe('Tests for <ResourceListBase>', () => {
|
|||
);
|
||||
|
||||
expect(component.find(ErrorBanner)).toHaveLength(0);
|
||||
expect(component.find(Spin)).toHaveLength(0);
|
||||
expect(component.find(Spinner)).toHaveLength(0);
|
||||
expect(component.find(MetricsTable)).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
@ -68,7 +68,7 @@ describe('Tests for <ResourceListBase>', () => {
|
|||
const metrics = component.find(MetricsTable);
|
||||
|
||||
expect(component.find(ErrorBanner)).toHaveLength(0);
|
||||
expect(component.find(Spin)).toHaveLength(0);
|
||||
expect(component.find(Spinner)).toHaveLength(0);
|
||||
expect(metrics).toHaveLength(1);
|
||||
|
||||
expect(metrics.props().resource).toEqual(resource);
|
||||
|
|
|
@ -1,43 +1,35 @@
|
|||
import _ from 'lodash';
|
||||
import BaseTable from './BaseTable.jsx';
|
||||
import CallToAction from './CallToAction.jsx';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import ErrorModal from './ErrorModal.jsx';
|
||||
import { incompleteMeshMessage } from './util/CopyUtils.jsx';
|
||||
import Metric from './Metric.jsx';
|
||||
import moment from 'moment';
|
||||
import { numericSort } from './util/Utils.js';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import MeshedStatusTable from './MeshedStatusTable.jsx';
|
||||
import Percentage from './util/Percentage.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Spinner from './util/Spinner.jsx';
|
||||
import StatusTable from './StatusTable.jsx';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import _ from 'lodash';
|
||||
import { incompleteMeshMessage } from './util/CopyUtils.jsx';
|
||||
import moment from 'moment';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { Col, Row, Spin, Table, Tooltip } from 'antd';
|
||||
import './../../css/service-mesh.css';
|
||||
|
||||
const serviceMeshDetailsColumns = [
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
key: "name"
|
||||
key: "name",
|
||||
render: d => d.name
|
||||
},
|
||||
{
|
||||
title: "Value",
|
||||
dataIndex: "value",
|
||||
key: "value",
|
||||
className: "numeric"
|
||||
isNumeric: true,
|
||||
render: d => d.value
|
||||
}
|
||||
];
|
||||
|
||||
const getClassification = (meshedPodCount, failedPodCount) => {
|
||||
if (failedPodCount > 0) {
|
||||
return "poor";
|
||||
} else if (meshedPodCount === 0) {
|
||||
return "neutral";
|
||||
} else {
|
||||
return "good";
|
||||
}
|
||||
};
|
||||
|
||||
const getPodClassification = pod => {
|
||||
if (pod.status === "Running") {
|
||||
return "good";
|
||||
|
@ -48,66 +40,6 @@ const getPodClassification = pod => {
|
|||
}
|
||||
};
|
||||
|
||||
const namespacesColumns = PrefixedLink => [
|
||||
{
|
||||
title: "Namespace",
|
||||
key: "namespace",
|
||||
defaultSortOrder: "ascend",
|
||||
sorter: (a, b) => (a.namespace || "").localeCompare(b.namespace),
|
||||
render: d => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PrefixedLink to={"/namespaces/" + d.namespace}>{d.namespace}</PrefixedLink>
|
||||
{ _.isEmpty(d.errors) ? null :
|
||||
<ErrorModal errors={d.errors} resourceName={d.namespace} resourceType="namespace" />
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Meshed pods",
|
||||
dataIndex: "meshedPodsStr",
|
||||
key: "meshedPodsStr",
|
||||
className: "numeric",
|
||||
sorter: (a, b) => numericSort(a.totalPods, b.totalPods),
|
||||
},
|
||||
{
|
||||
title: "Meshed Status",
|
||||
key: "meshification",
|
||||
sorter: (a, b) => numericSort(a.meshedPercent.get(), b.meshedPercent.get()),
|
||||
render: row => {
|
||||
let containerWidth = 132;
|
||||
let percent = row.meshedPercent.get();
|
||||
let barWidth = percent < 0 ? 0 : Math.round(percent * containerWidth);
|
||||
let barType = _.isEmpty(row.errors) ?
|
||||
getClassification(row.meshedPods, row.failedPods) : "poor";
|
||||
|
||||
|
||||
let percentMeshedMsg = "";
|
||||
if (row.meshedPercent.get() >= 0) {
|
||||
percentMeshedMsg = `(${row.meshedPercent.prettyRate()})`;
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
overlayStyle={{ fontSize: "12px" }}
|
||||
title={(
|
||||
<div>
|
||||
<div>
|
||||
{`${row.meshedPods} out of ${row.totalPods} running or pending pods are in the mesh ${percentMeshedMsg}`}
|
||||
</div>
|
||||
{row.failedPods === 0 ? null : <div>{ `${row.failedPods} failed pods` }</div>}
|
||||
</div>
|
||||
)}>
|
||||
<div className={"container-bar " + barType} style={{width: containerWidth}}>
|
||||
<div className={"inner-bar " + barType} style={{width: barWidth}}> </div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const componentsToDeployNames = {
|
||||
"Destination": "controller",
|
||||
"Grafana" : "grafana",
|
||||
|
@ -266,87 +198,63 @@ class ServiceMesh extends React.Component {
|
|||
|
||||
renderControlPlaneDetails() {
|
||||
return (
|
||||
<div className="mesh-section">
|
||||
<div className="clearfix header-with-metric">
|
||||
<div className="subsection-header">Control plane</div>
|
||||
<Metric title="Components" value={this.componentCount()} className="metric-large" />
|
||||
</div>
|
||||
<React.Fragment>
|
||||
<Grid container justify="space-between">
|
||||
<Grid item xs={3}>
|
||||
<Typography variant="h6">Control plane</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<Typography align="right">Components</Typography>
|
||||
<Typography align="right">{this.componentCount()}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<StatusTable
|
||||
data={this.state.components}
|
||||
statusColumnTitle="Pod Status"
|
||||
shouldLink={false}
|
||||
api={this.api} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderServiceMeshDetails() {
|
||||
return (
|
||||
<div className="mesh-section">
|
||||
<div className="clearfix header-with-metric">
|
||||
<div className="subsection-header">Service mesh details</div>
|
||||
</div>
|
||||
<React.Fragment>
|
||||
<Typography variant="h6">Service mesh details</Typography>
|
||||
|
||||
<div className="service-mesh-table">
|
||||
<Table
|
||||
className="metric-table"
|
||||
dataSource={this.getServiceMeshDetails()}
|
||||
columns={serviceMeshDetailsColumns}
|
||||
pagination={false}
|
||||
size="middle" />
|
||||
</div>
|
||||
</div>
|
||||
<BaseTable
|
||||
tableClassName="metric-table"
|
||||
tableRows={this.getServiceMeshDetails()}
|
||||
tableColumns={serviceMeshDetailsColumns}
|
||||
rowKey={d => d.key} />
|
||||
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderAddResourcesMessage() {
|
||||
let message = "";
|
||||
let numUnadded = 0;
|
||||
|
||||
if (_.isEmpty(this.state.nsStatuses)) {
|
||||
return <div className="mesh-completion-message">No resources detected.</div>;
|
||||
}
|
||||
|
||||
let meshedCount = _.countBy(this.state.nsStatuses, pod => {
|
||||
return pod.meshedPercent.get() > 0;
|
||||
});
|
||||
let numUnadded = meshedCount["false"] || 0;
|
||||
|
||||
if (numUnadded === 0) {
|
||||
return (
|
||||
<div className="mesh-completion-message">
|
||||
All namespaces have a {this.props.productName} install.
|
||||
</div>
|
||||
);
|
||||
message = "No resources detected.";
|
||||
} else {
|
||||
return (
|
||||
<div className="mesh-completion-message">
|
||||
{numUnadded} {numUnadded === 1 ? "namespace has" : "namespaces have"} no meshed resources.
|
||||
{incompleteMeshMessage()}
|
||||
</div>
|
||||
);
|
||||
let meshedCount = _.countBy(this.state.nsStatuses, pod => {
|
||||
return pod.meshedPercent.get() > 0;
|
||||
});
|
||||
numUnadded = meshedCount["false"] || 0;
|
||||
message = numUnadded === 0 ? `All namespaces have a ${this.props.productName} install.` :
|
||||
`${numUnadded} ${numUnadded === 1 ? "namespace has" : "namespaces have"} no meshed resources.`;
|
||||
}
|
||||
}
|
||||
|
||||
renderNamespaceStatusTable() {
|
||||
let rowCn = row => {
|
||||
return row.meshedPercent.get() > 0.9 ? "good" : "neutral";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mesh-section">
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
<Table
|
||||
className="metric-table service-mesh-table mesh-completion-table"
|
||||
dataSource={this.state.nsStatuses}
|
||||
columns={namespacesColumns(this.api.PrefixedLink)}
|
||||
rowKey="namespace"
|
||||
rowClassName={rowCn}
|
||||
pagination={false}
|
||||
size="middle" />
|
||||
</Col>
|
||||
<Col span={8}>{this.renderAddResourcesMessage()}</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography>{message}</Typography>
|
||||
{ numUnadded > 0 ? incompleteMeshMessage() : null }
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -354,19 +262,24 @@ class ServiceMesh extends React.Component {
|
|||
return (
|
||||
<div className="page-content">
|
||||
{ !this.state.error ? null : <ErrorBanner message={this.state.error} /> }
|
||||
{ !this.state.loaded ? <Spin size="large" /> : (
|
||||
{ !this.state.loaded ? <Spinner /> : (
|
||||
<div>
|
||||
{this.proxyCount() === 0 ?
|
||||
<CallToAction
|
||||
numResources={_.size(this.state.nsStatuses)}
|
||||
resource="namespace" /> : null}
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>{this.renderControlPlaneDetails()}</Col>
|
||||
<Col span={8}>{this.renderServiceMeshDetails()}</Col>
|
||||
</Row>
|
||||
<Grid container spacing={24}>
|
||||
<Grid item xs={8} container direction="column" >
|
||||
<Grid item>{this.renderControlPlaneDetails()}</Grid>
|
||||
<Grid item><MeshedStatusTable tableRows={_.sortBy(this.state.nsStatuses, "namespace")} /></Grid>
|
||||
</Grid>
|
||||
|
||||
{this.renderNamespaceStatusTable()}
|
||||
<Grid item xs={4} container direction="column" spacing={24}>
|
||||
<Grid item>{this.renderServiceMeshDetails()}</Grid>
|
||||
<Grid item>{this.renderAddResourcesMessage()}</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { routerWrap } from '../../test/testHelpers.jsx';
|
|||
import ServiceMesh from './ServiceMesh.jsx';
|
||||
import sinon from 'sinon';
|
||||
import sinonStubPromise from 'sinon-stub-promise';
|
||||
import Spinner from './util/Spinner.jsx';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
sinonStubPromise(sinon);
|
||||
|
@ -12,6 +13,9 @@ sinonStubPromise(sinon);
|
|||
describe('ServiceMesh', () => {
|
||||
let component, fetchStub;
|
||||
|
||||
// https://material-ui.com/style/typography/#migration-to-typography-v2
|
||||
window.__MUI_USE_NEXT_TYPOGRAPHY_VARIANTS__ = true;
|
||||
|
||||
function withPromise(fn) {
|
||||
return component.find("ServiceMesh").instance().serverPromise.then(fn);
|
||||
}
|
||||
|
@ -46,7 +50,7 @@ describe('ServiceMesh', () => {
|
|||
it("renders the spinner before metrics are loaded", () => {
|
||||
component = mount(routerWrap(ServiceMesh));
|
||||
|
||||
expect(component.find(".ant-spin")).toHaveLength(1);
|
||||
expect(component.find(Spinner)).toHaveLength(1);
|
||||
expect(component.find("ServiceMesh")).toHaveLength(1);
|
||||
expect(component.find("CallToAction")).toHaveLength(0);
|
||||
});
|
||||
|
@ -60,9 +64,9 @@ describe('ServiceMesh', () => {
|
|||
|
||||
return withPromise(() => {
|
||||
component.update();
|
||||
// console.log(component.find("Spin").debug());
|
||||
|
||||
expect(component.find("ServiceMesh")).toHaveLength(1);
|
||||
expect(component.find(".ant-spin")).toHaveLength(0);
|
||||
expect(component.find(Spinner)).toHaveLength(0);
|
||||
expect(component.find("CallToAction")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
@ -77,7 +81,7 @@ describe('ServiceMesh', () => {
|
|||
return withPromise(() => {
|
||||
component.update();
|
||||
expect(component.find("ServiceMesh")).toHaveLength(1);
|
||||
expect(component.find(".ant-spin")).toHaveLength(0);
|
||||
expect(component.find(Spinner)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -91,7 +95,7 @@ describe('ServiceMesh', () => {
|
|||
return withPromise(() => {
|
||||
component.update();
|
||||
expect(component.find("ServiceMesh")).toHaveLength(1);
|
||||
expect(component.find(".ant-spin")).toHaveLength(0);
|
||||
expect(component.find(Spinner)).toHaveLength(0);
|
||||
expect(component).toIncludeText("Service mesh details");
|
||||
expect(component).toIncludeText("ShinyProductName version");
|
||||
});
|
||||
|
@ -107,7 +111,7 @@ describe('ServiceMesh', () => {
|
|||
return withPromise(() => {
|
||||
component.update();
|
||||
expect(component.find("ServiceMesh")).toHaveLength(1);
|
||||
expect(component.find(".ant-spin")).toHaveLength(0);
|
||||
expect(component.find(Spinner)).toHaveLength(0);
|
||||
expect(component).toIncludeText("Control plane");
|
||||
});
|
||||
});
|
||||
|
@ -122,7 +126,7 @@ describe('ServiceMesh', () => {
|
|||
return withPromise(() => {
|
||||
component.update();
|
||||
expect(component.find("ServiceMesh")).toHaveLength(1);
|
||||
expect(component.find(".ant-spin")).toHaveLength(0);
|
||||
expect(component.find(Spinner)).toHaveLength(0);
|
||||
expect(component).toIncludeText("Data plane");
|
||||
});
|
||||
});
|
||||
|
@ -136,7 +140,7 @@ describe('ServiceMesh', () => {
|
|||
component = mount(routerWrap(ServiceMesh));
|
||||
|
||||
return withPromise(() => {
|
||||
expect(component).toIncludeText("No resources detected");
|
||||
expect(component).toIncludeText("No namespaces detected");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,351 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { friendlyTitle } from './util/Utils.js';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ReactRouterPropTypes from 'react-router-prop-types';
|
||||
import Version from './Version.jsx';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { Badge, Form, Icon, Layout, Menu, Select } from 'antd';
|
||||
import {
|
||||
excludeResourcesFromRollup,
|
||||
getSuccessRateClassification,
|
||||
processMultiResourceRollup,
|
||||
processSingleResourceRollup
|
||||
} from './util/MetricUtils.jsx';
|
||||
import {linkerdLogoOnly, linkerdWordLogo} from './util/SvgWrappers.jsx';
|
||||
import './../../css/sidebar.css';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
const classificationLabels = {
|
||||
good: "success",
|
||||
neutral: "warning",
|
||||
bad: "error",
|
||||
default: "default"
|
||||
};
|
||||
|
||||
class Sidebar extends React.Component {
|
||||
static defaultProps = {
|
||||
productName: 'controller'
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
api: PropTypes.shape({
|
||||
PrefixedLink: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
location: ReactRouterPropTypes.location.isRequired,
|
||||
pathPrefix: PropTypes.string.isRequired,
|
||||
productName: PropTypes.string,
|
||||
releaseVersion: PropTypes.string.isRequired,
|
||||
uuid: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = this.props.api;
|
||||
this.toggleCollapse = this.toggleCollapse.bind(this);
|
||||
this.loadFromServer = this.loadFromServer.bind(this);
|
||||
this.handleApiError = this.handleApiError.bind(this);
|
||||
this.handleNamespaceSelector = this.handleNamespaceSelector.bind(this);
|
||||
|
||||
this.state = this.getInitialState();
|
||||
}
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
pollingInterval: 12000,
|
||||
initialCollapse: false,
|
||||
collapsed: true,
|
||||
error: null,
|
||||
latestVersion: '',
|
||||
isLatest: true,
|
||||
pendingRequests: false,
|
||||
namespaceFilter: "all"
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchVersion();
|
||||
this.startServerPolling();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// the Sidebar never unmounts, but if something ever does, we should take care of it
|
||||
this.stopServerPolling();
|
||||
}
|
||||
|
||||
startServerPolling() {
|
||||
this.loadFromServer();
|
||||
this.timerId = window.setInterval(this.loadFromServer, this.state.pollingInterval);
|
||||
}
|
||||
|
||||
stopServerPolling() {
|
||||
window.clearInterval(this.timerId);
|
||||
this.api.cancelCurrentRequests();
|
||||
}
|
||||
|
||||
fetchVersion() {
|
||||
let versionUrl = `https://versioncheck.linkerd.io/version.json?version=${this.props.releaseVersion}&uuid=${this.props.uuid}&source=web`;
|
||||
fetch(versionUrl, { credentials: 'include' })
|
||||
.then(rsp => rsp.json())
|
||||
.then(versionRsp => {
|
||||
let latestVersion;
|
||||
let parts = this.props.releaseVersion.split("-", 2);
|
||||
if (parts.length === 2) {
|
||||
latestVersion = versionRsp[parts[0]];
|
||||
}
|
||||
this.setState({
|
||||
latestVersion,
|
||||
isLatest: latestVersion === this.props.releaseVersion
|
||||
});
|
||||
}).catch(this.handleApiError);
|
||||
}
|
||||
|
||||
loadFromServer() {
|
||||
if (this.state.pendingRequests) {
|
||||
return; // don't make more requests if the ones we sent haven't completed
|
||||
}
|
||||
this.setState({ pendingRequests: true });
|
||||
|
||||
this.api.setCurrentRequests([
|
||||
this.api.fetchMetrics(this.api.urlsForResource("all")),
|
||||
this.api.fetchMetrics(this.api.urlsForResource("namespace"))
|
||||
]);
|
||||
|
||||
// expose serverPromise for testing
|
||||
this.serverPromise = Promise.all(this.api.getCurrentPromises())
|
||||
.then(([allRsp, nsRsp]) => {
|
||||
let allResourceGroups = processMultiResourceRollup(allRsp);
|
||||
let finalResourceGroups = excludeResourcesFromRollup(allResourceGroups, ["authority", "service"]);
|
||||
let nsStats = processSingleResourceRollup(nsRsp);
|
||||
let namespaces = _(nsStats).map('name').sortBy().value();
|
||||
|
||||
this.setState({
|
||||
finalResourceGroups,
|
||||
namespaces,
|
||||
pendingRequests: false,
|
||||
});
|
||||
}).catch(this.handleApiError);
|
||||
}
|
||||
|
||||
handleApiError(e) {
|
||||
this.setState({
|
||||
pendingRequests: false,
|
||||
error: e
|
||||
});
|
||||
}
|
||||
|
||||
toggleCollapse() {
|
||||
if (this.state.initialCollapse) {
|
||||
// fix weird situation where toggleCollapsed is called on pageload,
|
||||
// causing the toggle states to be inconsistent. Don't toggle on the
|
||||
// very first call to toggleCollapse()
|
||||
this.setState({ initialCollapse: false});
|
||||
} else {
|
||||
this.setState({ collapsed: !this.state.collapsed });
|
||||
}
|
||||
}
|
||||
|
||||
handleNamespaceSelector(value) {
|
||||
this.setState({
|
||||
namespaceFilter: value
|
||||
});
|
||||
}
|
||||
|
||||
filterResourcesByNamespace(resources, namespace) {
|
||||
let resourceFilter = namespace === "all" ? r => r.added :
|
||||
r => r.namespace === namespace && r.added;
|
||||
|
||||
return _.mapValues(resources, o => _.filter(o, resourceFilter));
|
||||
}
|
||||
|
||||
render() {
|
||||
let normalizedPath = this.props.location.pathname.replace(this.props.pathPrefix, "");
|
||||
let PrefixedLink = this.api.PrefixedLink;
|
||||
let namespaces = [
|
||||
{value: "all", name: "All Namespaces"}
|
||||
].concat(_.map(this.state.namespaces, ns => {
|
||||
return {value: ns, name: ns};
|
||||
}));
|
||||
let sidebarComponents = this.filterResourcesByNamespace(this.state.finalResourceGroups, this.state.namespaceFilter);
|
||||
|
||||
return (
|
||||
<Layout.Sider
|
||||
width="260px"
|
||||
breakpoint="lg"
|
||||
collapsed={this.state.collapsed}
|
||||
collapsible={true}
|
||||
onCollapse={this.toggleCollapse}>
|
||||
<div className="sidebar">
|
||||
|
||||
<div className={`sidebar-menu-header ${this.state.collapsed ? "collapsed" : ""}`}>
|
||||
<PrefixedLink to="/overview">
|
||||
{this.state.collapsed ? linkerdLogoOnly : linkerdWordLogo}
|
||||
</PrefixedLink>
|
||||
</div>
|
||||
|
||||
<Menu
|
||||
className="sidebar-menu"
|
||||
theme="dark"
|
||||
selectedKeys={[normalizedPath]}>
|
||||
|
||||
<Menu.Item className="sidebar-menu-item" key="/overview">
|
||||
<PrefixedLink to="/overview">
|
||||
<Icon type="home" />
|
||||
<span>Overview</span>
|
||||
</PrefixedLink>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item className="sidebar-menu-item" key="/tap">
|
||||
<PrefixedLink to="/tap">
|
||||
<Icon><i className="fas fa-microscope" /></Icon>
|
||||
<span>Tap</span>
|
||||
</PrefixedLink>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item className="sidebar-menu-item" key="/top">
|
||||
<PrefixedLink to="/top">
|
||||
<Icon><i className="fas fa-stream" /></Icon>
|
||||
<span>Top</span>
|
||||
</PrefixedLink>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item className="sidebar-menu-item" key="/servicemesh">
|
||||
<PrefixedLink to="/servicemesh">
|
||||
<Icon type="cloud" />
|
||||
<span>Service mesh</span>
|
||||
</PrefixedLink>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.SubMenu
|
||||
className="sidebar-menu-item"
|
||||
key="byresource"
|
||||
title={<span className="sidebar-title"><Icon type="bars" />{this.state.collapsed ? "" : "Resources"}</span>}>
|
||||
<Menu.Item><PrefixedLink to="/authorities">Authorities</PrefixedLink></Menu.Item>
|
||||
<Menu.Item><PrefixedLink to="/deployments">Deployments</PrefixedLink></Menu.Item>
|
||||
<Menu.Item><PrefixedLink to="/namespaces">Namespaces</PrefixedLink></Menu.Item>
|
||||
<Menu.Item><PrefixedLink to="/pods">Pods</PrefixedLink></Menu.Item>
|
||||
<Menu.Item><PrefixedLink to="/replicationcontrollers">Replication Controllers</PrefixedLink></Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
<Menu.Divider />
|
||||
</Menu>
|
||||
|
||||
|
||||
<Menu
|
||||
className="sidebar-menu"
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
selectedKeys={[normalizedPath]}>
|
||||
{
|
||||
this.state.collapsed ? null : (
|
||||
<Menu.Item className="sidebar-menu-item" key="/namespace-selector">
|
||||
<Form layout="inline">
|
||||
<Form.Item>
|
||||
<Select
|
||||
defaultValue={this.state.namespaceFilter || "All Namespaces"}
|
||||
dropdownMatchSelectWidth={true}
|
||||
onChange={this.handleNamespaceSelector}>
|
||||
{
|
||||
_.map(namespaces, label => {
|
||||
return (
|
||||
<Select.Option key={label.value} value={label.value}>{label.name}</Select.Option>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Menu.Item>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
this.state.collapsed ? null :
|
||||
_.map(_.keys(sidebarComponents).sort(), resourceName => {
|
||||
return (
|
||||
<Menu.SubMenu
|
||||
className="sidebar-menu-item"
|
||||
key={resourceName}
|
||||
title={friendlyTitle(resourceName).plural}
|
||||
disabled={_.isEmpty(sidebarComponents[resourceName])}>
|
||||
{
|
||||
_.map(_.sortBy(sidebarComponents[resourceName], r => `${r.namespace}/${r.name}`), r => {
|
||||
// only display resources that have been meshed
|
||||
return (
|
||||
<Menu.Item
|
||||
className="sidebar-submenu-item"
|
||||
title={`${r.namespace}/${r.name}`}
|
||||
key={this.api.generateResourceURL(r)}>
|
||||
<div>
|
||||
<PrefixedLink
|
||||
to={this.api.generateResourceURL(r)}>
|
||||
{`${r.namespace}/${r.name}`}
|
||||
</PrefixedLink>
|
||||
<Badge status={getSuccessRateClassification(r.successRate, classificationLabels)} />
|
||||
</div>
|
||||
</Menu.Item>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Menu.Item className="sidebar-menu-item" key="/docs">
|
||||
<Link to="https://linkerd.io/2/overview/" target="_blank">
|
||||
<Icon type="file-text" />
|
||||
<span>Documentation</span>
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.SubMenu
|
||||
className="sidebar-menu-item"
|
||||
key="help"
|
||||
title={<span className="sidebar-title"><Icon type="question-circle" />{this.state.collapsed ? "" : "Help"}</span>}>
|
||||
<Menu.Item>
|
||||
<Link to="https://lists.cncf.io/g/cncf-linkerd-users" target="_blank">
|
||||
Join the mailing list
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Link to="https://slack.linkerd.io" target="_blank">
|
||||
Join the slack
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Link to="https://github.com/linkerd/linkerd2/issues/new/choose" target="_blank">
|
||||
File an issue
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
|
||||
{
|
||||
this.state.isLatest ? null : (
|
||||
<Menu.Item className="sidebar-menu-item" key="/update">
|
||||
<Link to="https://versioncheck.linkerd.io/update" target="_blank">
|
||||
<Icon type="exclamation-circle-o" className="update" />
|
||||
<span>Update {this.props.productName}</span>
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
)
|
||||
}
|
||||
</Menu>
|
||||
|
||||
{
|
||||
this.state.collapsed ? null : (
|
||||
<Version
|
||||
isLatest={this.state.isLatest}
|
||||
latestVersion={this.state.latestVersion}
|
||||
releaseVersion={this.props.releaseVersion}
|
||||
error={this.state.error}
|
||||
uuid={this.props.uuid} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Layout.Sider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withContext(Sidebar);
|
|
@ -1,70 +0,0 @@
|
|||
import ApiHelpers from "./util/ApiHelpers.jsx";
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import namespaceFixtures from '../../test/fixtures/namespaces.json';
|
||||
import React from "react";
|
||||
import { Select } from 'antd';
|
||||
import Sidebar from "./Sidebar.jsx";
|
||||
import sinon from "sinon";
|
||||
import sinonStubPromise from "sinon-stub-promise";
|
||||
import { mount } from "enzyme";
|
||||
|
||||
sinonStubPromise(sinon);
|
||||
|
||||
const loc = {
|
||||
pathname: '',
|
||||
hash: '',
|
||||
pathPrefix: '',
|
||||
search: '',
|
||||
};
|
||||
|
||||
describe('Sidebar', () => {
|
||||
let curVer = "v1.2.3";
|
||||
let component, fetchStub;
|
||||
let apiHelpers = ApiHelpers("");
|
||||
|
||||
const openNamespaceSelector = component => {
|
||||
// click trigger to expand the namespace selector
|
||||
component.find(Select).simulate('click');
|
||||
};
|
||||
|
||||
function withPromise(fn) {
|
||||
return component.find("Sidebar").instance().serverPromise.then(fn);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fetchStub = sinon.stub(window, 'fetch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
component = null;
|
||||
window.fetch.restore();
|
||||
});
|
||||
|
||||
it("namespace selector has options", () => {
|
||||
fetchStub.resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(namespaceFixtures)
|
||||
});
|
||||
|
||||
component = mount(
|
||||
<BrowserRouter>
|
||||
<Sidebar
|
||||
location={loc}
|
||||
api={apiHelpers}
|
||||
releaseVersion={curVer}
|
||||
pathPrefix=""
|
||||
uuid="fakeuuid" />
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
return withPromise(() => {
|
||||
openNamespaceSelector(component);
|
||||
|
||||
// number of namespaces in api result
|
||||
expect(namespaceFixtures.ok.statTables[0].podGroup.rows).toHaveLength(5);
|
||||
|
||||
// plus "All namespaces" option
|
||||
expect(component.find(".ant-select-dropdown-menu-item")).toHaveLength(6);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,7 +1,8 @@
|
|||
import _ from 'lodash';
|
||||
import BaseTable from './BaseTable.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Table, Tooltip } from 'antd';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import _ from 'lodash';
|
||||
|
||||
const columnConfig = {
|
||||
"Pod Status": {
|
||||
|
@ -36,10 +37,9 @@ const StatusDot = ({status, multilineDots, columnName}) => (
|
|||
<div>{_.get(columnConfig, [columnName, "dotExplanation"])(status)}</div>
|
||||
<div>Uptime: {status.uptime} ({status.uptimeSec}s)</div>
|
||||
</div>
|
||||
)}
|
||||
overlayStyle={{ fontSize: "12px" }}>
|
||||
)}>
|
||||
<div
|
||||
className={`status-dot status-dot-${status.value} ${multilineDots ? 'dot-multiline': ''}`}
|
||||
className={`status-table-dot status-dot status-dot-${status.value} ${multilineDots ? 'dot-multiline': ''}`}
|
||||
key={status.name}>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
@ -57,22 +57,23 @@ StatusDot.propTypes = {
|
|||
const columns = {
|
||||
resourceName: {
|
||||
title: "Deployment",
|
||||
dataIndex: "name",
|
||||
key: "name"
|
||||
key: "name",
|
||||
render: d => d.name
|
||||
},
|
||||
pods: {
|
||||
title: "Pods",
|
||||
dataIndex: "numEntities"
|
||||
key: "pods",
|
||||
isNumeric: true,
|
||||
render: d => d.numEntities
|
||||
},
|
||||
status: name => {
|
||||
return {
|
||||
title: name,
|
||||
dataIndex: "pods",
|
||||
width: columnConfig[name].width,
|
||||
render: statuses => {
|
||||
let multilineDots = _.size(statuses) > columnConfig[name].wrapDotsAt;
|
||||
key: "status",
|
||||
render: d => {
|
||||
let multilineDots = _.size(d.pods) > columnConfig[name].wrapDotsAt;
|
||||
|
||||
return _.map(statuses, (status, i) => {
|
||||
return _.map(d.pods, (status, i) => {
|
||||
return (
|
||||
<StatusDot
|
||||
status={status}
|
||||
|
@ -114,13 +115,11 @@ class StatusTable extends React.Component {
|
|||
let tableData = this.getTableData();
|
||||
|
||||
return (
|
||||
<Table
|
||||
dataSource={tableData}
|
||||
columns={tableCols}
|
||||
pagination={false}
|
||||
className="metric-table"
|
||||
rowKey={r => r.name}
|
||||
size="middle" />
|
||||
<BaseTable
|
||||
tableRows={tableData}
|
||||
tableColumns={tableCols}
|
||||
tableClassName="metric-table"
|
||||
rowKey={r => r.name} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import _ from 'lodash';
|
||||
import { UrlQueryParamTypes, addUrlProps } from 'react-url-query';
|
||||
import { emptyTapQuery, processTapEvent, setMaxRps, wsCloseCodes } from './util/TapUtils.jsx';
|
||||
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TapEventTable from './TapEventTable.jsx';
|
||||
import TapQueryCliCmd from './TapQueryCliCmd.jsx';
|
||||
import TapQueryForm from './TapQueryForm.jsx';
|
||||
import _ from 'lodash';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { addUrlProps, UrlQueryParamTypes } from 'react-url-query';
|
||||
import { emptyTapQuery, processTapEvent, setMaxRps, wsCloseCodes } from './util/TapUtils.jsx';
|
||||
import './../../css/tap.css';
|
||||
|
||||
const urlPropsQueryConfig = {
|
||||
autostart: { type: UrlQueryParamTypes.string }
|
||||
|
@ -169,6 +168,7 @@ class Tap extends React.Component {
|
|||
resultIndex[d.id][d.eventType] = d;
|
||||
// assumption: requests of a given id all share the same high level metadata
|
||||
resultIndex[d.id]["base"] = d;
|
||||
resultIndex[d.id].key = d.id;
|
||||
resultIndex[d.id].lastUpdated = Date.now();
|
||||
}
|
||||
|
||||
|
@ -313,8 +313,6 @@ class Tap extends React.Component {
|
|||
updateQuery={this.updateQuery}
|
||||
query={this.state.query} />
|
||||
|
||||
<TapQueryCliCmd cmdName="tap" query={this.state.query} />
|
||||
|
||||
<TapEventTable
|
||||
resource={this.state.query.resource}
|
||||
tableRows={tableRows} />
|
||||
|
|
|
@ -1,11 +1,21 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { Col, Icon, Row, Table } from 'antd';
|
||||
import { directionColumn, srcDstColumn } from './util/TapUtils.jsx';
|
||||
import { formatLatencySec, formatWithComma } from './util/Utils.js';
|
||||
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import ExpandableTable from './ExpandableTable.jsx';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import _ from 'lodash';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
// https://godoc.org/google.golang.org/grpc/codes#Code
|
||||
const grpcStatusCodes = {
|
||||
0: "OK",
|
||||
|
@ -27,59 +37,66 @@ const grpcStatusCodes = {
|
|||
16: "Unauthenticated"
|
||||
};
|
||||
|
||||
const smallMetricColWidth = "120px";
|
||||
const spinnerStyles = theme => ({
|
||||
progress: {
|
||||
margin: theme.spacing.unit * 2,
|
||||
},
|
||||
});
|
||||
const SpinnerBase = () => <CircularProgress size={20} />;
|
||||
const Spinner = withStyles(spinnerStyles)(SpinnerBase);
|
||||
|
||||
const httpStatusCol = {
|
||||
title: "HTTP status",
|
||||
key: "http-status",
|
||||
width: smallMetricColWidth,
|
||||
dataIndex: "responseInit.http.responseInit",
|
||||
render: d => !d ? <Icon type="loading" /> : d.httpStatus
|
||||
render: datum => {
|
||||
let d = _.get(datum, "responseInit.http.responseInit");
|
||||
return !d ? <Spinner /> : d.httpStatus;
|
||||
}
|
||||
};
|
||||
|
||||
const responseInitLatencyCol = {
|
||||
title: "Latency",
|
||||
key: "rsp-latency",
|
||||
width: smallMetricColWidth,
|
||||
dataIndex: "responseInit.http.responseInit",
|
||||
render: d => !d ? <Icon type="loading" /> : formatTapLatency(d.sinceRequestInit)
|
||||
isNumeric: true,
|
||||
render: datum => {
|
||||
let d = _.get(datum, "responseInit.http.responseInit");
|
||||
return !d ? <Spinner /> : formatTapLatency(d.sinceRequestInit);
|
||||
}
|
||||
};
|
||||
|
||||
const grpcStatusCol = {
|
||||
title: "GRPC status",
|
||||
key: "grpc-status",
|
||||
width: smallMetricColWidth,
|
||||
dataIndex: "responseEnd.http.responseEnd",
|
||||
render: d => !d ? <Icon type="loading" /> :
|
||||
_.isNull(d.eos) ? "---" : grpcStatusCodes[_.get(d, "eos.grpcStatusCode")]
|
||||
render: datum => {
|
||||
let d = _.get(datum, "responseEnd.http.responseEnd");
|
||||
return !d ? <Spinner /> :
|
||||
_.isNull(d.eos) ? "---" : grpcStatusCodes[_.get(d, "eos.grpcStatusCode")];
|
||||
}
|
||||
};
|
||||
|
||||
const pathCol = {
|
||||
title: "Path",
|
||||
key: "path",
|
||||
dataIndex: "requestInit.http.requestInit",
|
||||
render: d => !d ? <Icon type="loading" /> : d.path
|
||||
render: datum => {
|
||||
let d = _.get(datum, "requestInit.http.requestInit");
|
||||
return !d ? <Spinner /> : d.path;
|
||||
}
|
||||
};
|
||||
|
||||
const methodCol = {
|
||||
title: "Method",
|
||||
key: "method",
|
||||
dataIndex: "requestInit.http.requestInit",
|
||||
render: d => !d ? <Icon type="loading" /> : _.get(d, "method.registered")
|
||||
render: datum => {
|
||||
let d = _.get(datum, "requestInit.http.requestInit");
|
||||
return !d ? <Spinner /> : _.get(d, "method.registered");
|
||||
}
|
||||
};
|
||||
|
||||
const topLevelColumns = (resourceType, ResourceLink) => [
|
||||
{
|
||||
title: "Direction",
|
||||
key: "direction",
|
||||
dataIndex: "base.proxyDirection",
|
||||
width: "98px",
|
||||
filters: [
|
||||
{ text: "FROM", value: "INBOUND" },
|
||||
{ text: "TO", value: "OUTBOUND" }
|
||||
],
|
||||
render: directionColumn,
|
||||
onFilter: (value, row) => _.get(row, "base.proxyDirection").includes(value)
|
||||
render: d => directionColumn(d.base.proxyDirection)
|
||||
},
|
||||
{
|
||||
title: "Name",
|
||||
|
@ -108,72 +125,73 @@ const formatTapLatency = str => {
|
|||
return formatLatencySec(str.replace("s", ""));
|
||||
};
|
||||
|
||||
const itemDisplay = (title, value) => {
|
||||
return (
|
||||
<ListItem disableGutters>
|
||||
<ListItemText primary={title} secondary={value} />
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
const requestInitSection = d => (
|
||||
<React.Fragment>
|
||||
<Row gutter={8} className="tap-info-section">
|
||||
<h3>Request Init</h3>
|
||||
<Row gutter={8} className="expand-section-header">
|
||||
<Col span={8}>Authority</Col>
|
||||
<Col span={9}>Path</Col>
|
||||
<Col span={2}>Scheme</Col>
|
||||
<Col span={2}>Method</Col>
|
||||
<Col span={3}>TLS</Col>
|
||||
</Row>
|
||||
<Row gutter={8}>
|
||||
<Col span={8}>{_.get(d, "requestInit.http.requestInit.authority")}</Col>
|
||||
<Col span={9}>{_.get(d, "requestInit.http.requestInit.path")}</Col>
|
||||
<Col span={2}>{_.get(d, "requestInit.http.requestInit.scheme.registered")}</Col>
|
||||
<Col span={2}>{_.get(d, "requestInit.http.requestInit.method.registered")}</Col>
|
||||
<Col span={3}>{_.get(d, "base.tls")}</Col>
|
||||
</Row>
|
||||
</Row>
|
||||
<Typography variant="subtitle2">Request Init</Typography>
|
||||
<br />
|
||||
<List dense>
|
||||
{itemDisplay("Authority", _.get(d, "requestInit.http.requestInit.authority"))}
|
||||
{itemDisplay("Path", _.get(d, "requestInit.http.requestInit.path"))}
|
||||
{itemDisplay("Scheme", _.get(d, "requestInit.http.requestInit.scheme.registered"))}
|
||||
{itemDisplay("Method", _.get(d, "requestInit.http.requestInit.method.registered"))}
|
||||
{itemDisplay("TLS", _.get(d, "base.tls"))}
|
||||
</List>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
const responseInitSection = d => _.isEmpty(d.responseInit) ? null : (
|
||||
<React.Fragment>
|
||||
<hr />
|
||||
<Row gutter={8} className="tap-info-section">
|
||||
<h3>Response Init</h3>
|
||||
<Row gutter={8} className="expand-section-header">
|
||||
<Col span={4}>HTTP Status</Col>
|
||||
<Col span={4}>Latency</Col>
|
||||
</Row>
|
||||
<Row gutter={8}>
|
||||
<Col span={4}>{_.get(d, "responseInit.http.responseInit.httpStatus")}</Col>
|
||||
<Col span={4}>{formatTapLatency(_.get(d, "responseInit.http.responseInit.sinceRequestInit"))}</Col>
|
||||
</Row>
|
||||
</Row>
|
||||
<Typography variant="subtitle2">Response Init</Typography>
|
||||
<br />
|
||||
<List dense>
|
||||
{itemDisplay("HTTP Status", _.get(d, "responseInit.http.responseInit.httpStatus"))}
|
||||
{itemDisplay("Latency", formatTapLatency(_.get(d, "responseInit.http.responseInit.sinceRequestInit")))}
|
||||
</List>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
const responseEndSection = d => _.isEmpty(d.responseEnd) ? null : (
|
||||
<React.Fragment>
|
||||
<hr />
|
||||
<Row gutter={8} className="tap-info-section">
|
||||
<h3>Response End</h3>
|
||||
<Row gutter={8} className="expand-section-header">
|
||||
<Col span={4}>GRPC Status</Col>
|
||||
<Col span={4}>Latency</Col>
|
||||
<Col span={4}>Response Length (B)</Col>
|
||||
</Row>
|
||||
<Row gutter={8}>
|
||||
<Col span={4}>{_.isNull(_.get(d, "responseEnd.http.responseEnd.eos")) ? "N/A" : grpcStatusCodes[_.get(d, "responseEnd.http.responseEnd.eos.grpcStatusCode")]}</Col>
|
||||
<Col span={4}>{formatTapLatency(_.get(d, "responseEnd.http.responseEnd.sinceResponseInit"))}</Col>
|
||||
<Col span={4}>{formatWithComma(_.get(d, "responseEnd.http.responseEnd.responseBytes"))}</Col>
|
||||
</Row>
|
||||
</Row>
|
||||
<Typography variant="subtitle2">Response End</Typography>
|
||||
<br />
|
||||
|
||||
<List dense>
|
||||
{itemDisplay("GRPC Status", _.isNull(_.get(d, "responseEnd.http.responseEnd.eos")) ? "N/A" : grpcStatusCodes[_.get(d, "responseEnd.http.responseEnd.eos.grpcStatusCode")])}
|
||||
{itemDisplay("Latency", formatTapLatency(_.get(d, "responseEnd.http.responseEnd.sinceResponseInit")))}
|
||||
{itemDisplay("Response Length (B)", formatWithComma(_.get(d, "responseEnd.http.responseEnd.responseBytes")))}
|
||||
</List>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
|
||||
// hide verbose information
|
||||
const expandedRowRender = d => {
|
||||
return (
|
||||
<div className="tap-more-info">
|
||||
{requestInitSection(d)}
|
||||
{responseInitSection(d)}
|
||||
{responseEndSection(d)}
|
||||
</div>
|
||||
<Grid container spacing={16} className="tap-more-info">
|
||||
<Grid item xs={4}>
|
||||
<Card>
|
||||
<CardContent>{requestInitSection(d)}</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Card>
|
||||
<CardContent>{responseInitSection(d)}</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Card>
|
||||
<CardContent>{responseEndSection(d)}</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -192,17 +210,16 @@ class TapEventTable extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
let resourceType = this.props.resource.split("/")[0];
|
||||
const { tableRows, resource, api } = this.props;
|
||||
let resourceType = resource.split("/")[0];
|
||||
let columns = tapColumns(resourceType, api.ResourceLink);
|
||||
|
||||
return (
|
||||
<Table
|
||||
dataSource={this.props.tableRows}
|
||||
columns={tapColumns(resourceType, this.props.api.ResourceLink)}
|
||||
expandedRowRender={expandedRowRender} // hide extra info in expandable row
|
||||
expandRowByClick={true} // click anywhere on row to expand
|
||||
rowKey={r => r.base.id}
|
||||
pagination={false}
|
||||
className="tap-event-table"
|
||||
size="middle" />
|
||||
<ExpandableTable
|
||||
tableRows={tableRows}
|
||||
tableColumns={columns}
|
||||
expandedRowRender={expandedRowRender}
|
||||
tableClassName="metric-table" />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
const TapLink = ({PrefixedLink, namespace, resource, toNamespace, toResource, path}) => {
|
||||
let params = {
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import _ from 'lodash';
|
||||
import {
|
||||
CardContent,
|
||||
Typography
|
||||
} from '@material-ui/core';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import { tapQueryPropType } from './util/TapUtils.jsx';
|
||||
|
||||
/*
|
||||
|
@ -45,27 +50,27 @@ export default class TapQueryCliCmd extends React.Component {
|
|||
} = this.props.query;
|
||||
|
||||
return (
|
||||
<div className="tap-query">
|
||||
{
|
||||
_.isEmpty(resource) ? null :
|
||||
<React.Fragment>
|
||||
<div>Current {_.startCase(this.props.cmdName)} query:</div>
|
||||
<code>
|
||||
_.isEmpty(resource) ? null :
|
||||
<CardContent className="tap-query">
|
||||
<Typography variant="caption" gutterBottom>
|
||||
Current {_.startCase(this.props.cmdName)} query
|
||||
</Typography>
|
||||
|
||||
<code>
|
||||
linkerd {this.props.cmdName} {resource}
|
||||
{ resource.indexOf("namespace") === 0 ? null : this.renderCliItem("--namespace", namespace) }
|
||||
{ this.renderCliItem("--to", toResource) }
|
||||
{ _.isEmpty(toResource) || toResource.indexOf("namespace") === 0 ? null :
|
||||
{ resource.indexOf("namespace") === 0 ? null : this.renderCliItem("--namespace", namespace) }
|
||||
{ this.renderCliItem("--to", toResource) }
|
||||
{
|
||||
_.isEmpty(toResource) || toResource.indexOf("namespace") === 0 ? null :
|
||||
this.renderCliItem("--to-namespace", toNamespace)
|
||||
}
|
||||
{ this.renderCliItem("--method", method) }
|
||||
{ this.renderCliItem("--scheme", scheme) }
|
||||
{ this.renderCliItem("--authority", authority) }
|
||||
{ this.renderCliItem("--path", path) }
|
||||
{ this.renderCliItem("--max-rps", maxRps) }
|
||||
</code>
|
||||
</React.Fragment>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{ this.renderCliItem("--method", method) }
|
||||
{ this.renderCliItem("--scheme", scheme) }
|
||||
{ this.renderCliItem("--authority", authority) }
|
||||
{ this.renderCliItem("--path", path) }
|
||||
{ this.renderCliItem("--max-rps", maxRps) }
|
||||
</code>
|
||||
</CardContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,34 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { addUrlProps, UrlQueryParamTypes } from 'react-url-query';
|
||||
import {
|
||||
AutoComplete,
|
||||
Button,
|
||||
Col,
|
||||
Form,
|
||||
Icon,
|
||||
Input,
|
||||
Row,
|
||||
Select
|
||||
} from 'antd';
|
||||
Card,
|
||||
CardContent,
|
||||
ExpansionPanel,
|
||||
ExpansionPanelDetails,
|
||||
ExpansionPanelSummary,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Grid,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography
|
||||
} from '@material-ui/core';
|
||||
import { UrlQueryParamTypes, addUrlProps } from 'react-url-query';
|
||||
import {
|
||||
defaultMaxRps,
|
||||
emptyTapQuery,
|
||||
httpMethods,
|
||||
tapQueryProps,
|
||||
tapQueryPropType
|
||||
tapQueryPropType,
|
||||
tapQueryProps
|
||||
} from './util/TapUtils.jsx';
|
||||
|
||||
const colSpan = 5;
|
||||
const rowGutter = 16;
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TapQueryCliCmd from './TapQueryCliCmd.jsx';
|
||||
import _ from 'lodash';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
// you can also tap resources to tap all pods in the resource
|
||||
const resourceTypes = [
|
||||
|
@ -40,8 +47,44 @@ const urlPropsQueryConfig = _.mapValues(tapQueryProps, () => {
|
|||
return { type: UrlQueryParamTypes.string };
|
||||
});
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
formControl: {
|
||||
margin: theme.spacing.unit,
|
||||
minWidth: 200,
|
||||
},
|
||||
selectEmpty: {
|
||||
'margin-top': '32px'
|
||||
},
|
||||
card: {
|
||||
maxWidth: "100%",
|
||||
},
|
||||
actions: {
|
||||
display: 'flex',
|
||||
'padding-left': '32px'
|
||||
},
|
||||
expand: {
|
||||
transform: 'rotate(0deg)',
|
||||
transition: theme.transitions.create('transform', {
|
||||
duration: theme.transitions.duration.shortest,
|
||||
}),
|
||||
marginLeft: 'auto',
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
marginRight: -8,
|
||||
},
|
||||
},
|
||||
expandOpen: {
|
||||
transform: 'rotate(180deg)',
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
class TapQueryForm extends React.Component {
|
||||
static propTypes = {
|
||||
classes: PropTypes.shape({}).isRequired,
|
||||
enableAdvancedForm: PropTypes.bool,
|
||||
handleTapClear: PropTypes.func,
|
||||
handleTapStart: PropTypes.func.isRequired,
|
||||
|
@ -58,31 +101,6 @@ class TapQueryForm extends React.Component {
|
|||
tapIsClosing: false
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
let query = _.merge({}, props.query, _.pick(this.props, _.keys(tapQueryProps)));
|
||||
props.updateQuery(query);
|
||||
|
||||
let showAdvancedForm = _.some(
|
||||
_.omit(query, ['namespace', 'resource']),
|
||||
v => !_.isEmpty(v));
|
||||
|
||||
this.state = {
|
||||
query,
|
||||
showAdvancedForm,
|
||||
authoritiesByNs: {},
|
||||
resourcesByNs: {},
|
||||
autocomplete: {
|
||||
namespace: [],
|
||||
resource: [],
|
||||
toNamespace: [],
|
||||
toResource: [],
|
||||
authority: []
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (!_.isEqual(props.resourcesByNs, state.resourcesByNs)) {
|
||||
let resourcesByNs = props.resourcesByNs;
|
||||
|
@ -108,29 +126,49 @@ class TapQueryForm extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
toggleAdvancedForm = show => {
|
||||
this.setState({
|
||||
showAdvancedForm: show
|
||||
});
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
let query = _.merge({}, props.query, _.pick(this.props, _.keys(tapQueryProps)));
|
||||
props.updateQuery(query);
|
||||
|
||||
let advancedFormExpanded = _.some(
|
||||
_.omit(query, ['namespace', 'resource']),
|
||||
v => !_.isEmpty(v));
|
||||
|
||||
this.state = {
|
||||
query,
|
||||
advancedFormExpanded,
|
||||
authoritiesByNs: {},
|
||||
resourcesByNs: {},
|
||||
autocomplete: {
|
||||
namespace: [],
|
||||
resource: [],
|
||||
toNamespace: [],
|
||||
toResource: [],
|
||||
authority: []
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
handleFormChange = (name, scopeResource, shouldScopeAuthority) => {
|
||||
handleFormChange = (name, scopeResource) => {
|
||||
let state = {
|
||||
query: this.state.query,
|
||||
autocomplete: this.state.autocomplete
|
||||
};
|
||||
|
||||
return formVal => {
|
||||
let shouldScopeAuthority = name === "namespace";
|
||||
|
||||
return event => {
|
||||
let formVal = event.target.value;
|
||||
state.query[name] = formVal;
|
||||
this.handleUrlUpdate(name, formVal);
|
||||
|
||||
if (!_.isNil(scopeResource)) {
|
||||
// scope the available typeahead resources to the selected namespace
|
||||
state.autocomplete[scopeResource] = this.state.resourcesByNs[formVal];
|
||||
if (_.isEmpty(state.query[scopeResource]) || state.query[scopeResource].indexOf("namespace") !== -1) {
|
||||
state.query[scopeResource] = `namespace/${formVal}`;
|
||||
this.handleUrlUpdate(scopeResource, `namespace/${formVal}`);
|
||||
}
|
||||
state.query[scopeResource] = `namespace/${formVal}`;
|
||||
this.handleUrlUpdate(scopeResource, `namespace/${formVal}`);
|
||||
}
|
||||
|
||||
if (shouldScopeAuthority) {
|
||||
|
@ -162,6 +200,10 @@ class TapQueryForm extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
handleAdvancedFormExpandClick = () => {
|
||||
this.setState(state => ({advancedFormExpanded: !state.advancedFormExpanded}));
|
||||
}
|
||||
|
||||
autoCompleteData = name => {
|
||||
return _(this.state.autocomplete[name])
|
||||
.filter(d => d.indexOf(this.state.query[name]) !== -1)
|
||||
|
@ -183,106 +225,10 @@ class TapQueryForm extends React.Component {
|
|||
this.props.handleTapClear();
|
||||
}
|
||||
|
||||
renderAdvancedTapForm = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Row gutter={rowGutter}>
|
||||
<Col span={colSpan}>
|
||||
<Form.Item>
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="To Namespace"
|
||||
optionFilterProp="children"
|
||||
onChange={this.handleFormChange("toNamespace", "toResource")}
|
||||
value={this.state.query.toNamespace}>
|
||||
{
|
||||
_.map(this.state.autocomplete.toNamespace, (n, i) => (
|
||||
<Select.Option key={`ns-dr-${i}`} value={n}>{n}</Select.Option>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={colSpan}>
|
||||
<Form.Item>
|
||||
{this.renderResourceSelect("toResource", "toNamespace")}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={rowGutter}>
|
||||
<Col span={2 * colSpan}>
|
||||
<Form.Item
|
||||
extra="Display requests with this :authority">
|
||||
|
||||
<AutoComplete
|
||||
dataSource={this.autoCompleteData("authority")}
|
||||
onSelect={this.handleFormChange("authority")}
|
||||
onSearch={this.handleFormChange("authority")}
|
||||
placeholder="Authority"
|
||||
value={this.state.query.authority} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={2 * colSpan}>
|
||||
<Form.Item
|
||||
extra="Display requests with paths that start with this prefix">
|
||||
<Input
|
||||
placeholder="Path"
|
||||
onChange={this.handleFormEvent("path")}
|
||||
value={this.state.query.path} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
</Row>
|
||||
|
||||
<Row gutter={rowGutter}>
|
||||
<Col span={colSpan}>
|
||||
<Form.Item
|
||||
extra="Display requests with this scheme">
|
||||
<Input
|
||||
placeholder="Scheme"
|
||||
onChange={this.handleFormEvent("scheme")}
|
||||
value={this.state.query.scheme} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={colSpan}>
|
||||
<Form.Item
|
||||
extra={`Maximum requests per second to tap (default ${defaultMaxRps})`}>
|
||||
<Input
|
||||
placeholder="Max RPS"
|
||||
onChange={this.handleFormEvent("maxRps")}
|
||||
value={this.state.query.maxRps} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={colSpan}>
|
||||
<Form.Item
|
||||
extra="Display requests with this HTTP method">
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="HTTP method"
|
||||
onChange={this.handleFormChange("method")}
|
||||
value={this.state.query.method}>
|
||||
{
|
||||
_.map(httpMethods, m =>
|
||||
<Select.Option key={`method-select-${m}`} value={m}>{m}</Select.Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderResourceSelect = (resourceKey, namespaceKey) => {
|
||||
let selectedNs = this.state.query[namespaceKey];
|
||||
let nsEmpty = _.isNil(selectedNs) || _.isEmpty(selectedNs);
|
||||
let { classes } = this.props;
|
||||
|
||||
let resourceOptions = _.concat(
|
||||
resourceTypes,
|
||||
|
@ -291,36 +237,53 @@ class TapQueryForm extends React.Component {
|
|||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
disabled={nsEmpty}
|
||||
value={nsEmpty ? _.startCase(resourceKey) : this.state.query[resourceKey]}
|
||||
placeholder={_.startCase(resourceKey)}
|
||||
optionFilterProp="children"
|
||||
onChange={this.handleFormChange(resourceKey)}>
|
||||
{
|
||||
_.map(_.sortBy(resourceOptions), resource => (
|
||||
<Select.Option
|
||||
key={`${resourceKey}-${resource}`}
|
||||
value={resource}>{resource}
|
||||
</Select.Option>
|
||||
)
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
<React.Fragment>
|
||||
<InputLabel htmlFor={resourceKey}>{_.startCase(resourceKey)}</InputLabel>
|
||||
<Select
|
||||
value={nsEmpty ? _.startCase(resourceKey) : this.state.query[resourceKey]}
|
||||
onChange={this.handleFormChange(resourceKey)}
|
||||
inputProps={{ name: resourceKey, id: resourceKey }}
|
||||
className={classes.selectEmpty}>
|
||||
{
|
||||
_.map(_.sortBy(resourceOptions), resource => (
|
||||
<MenuItem key={`${namespaceKey}-${resourceKey}-${resource}`} value={resource}>{resource}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderNamespaceSelect = (title, namespaceKey, resourceKey) => {
|
||||
let { classes } = this.props;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<InputLabel htmlFor={namespaceKey}>{title}</InputLabel>
|
||||
<Select
|
||||
value={this.state.query[namespaceKey]}
|
||||
onChange={this.handleFormChange(namespaceKey, resourceKey)}
|
||||
inputProps={{ name: namespaceKey, id: namespaceKey }}
|
||||
className={classes.selectEmpty}>
|
||||
{
|
||||
_.map(this.state.autocomplete[namespaceKey], (n, i) => (
|
||||
<MenuItem key={`ns-dr-${i}`} value={n}>{n}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderTapButton = (tapInProgress, tapIsClosing) => {
|
||||
if (tapIsClosing) {
|
||||
return (<Button type="primary" className="tap-ctrl tap-stop" disabled={true}>Stop</Button>);
|
||||
return (<Button variant="outlined" color="primary" className="tap-ctrl tap-stop" disabled={true}>Stop</Button>);
|
||||
} else if (tapInProgress) {
|
||||
return (<Button type="primary" className="tap-ctrl tap-stop" onClick={this.props.handleTapStop}>Stop</Button>);
|
||||
return (<Button variant="outlined" color="primary" className="tap-ctrl tap-stop" onClick={this.props.handleTapStop}>Stop</Button>);
|
||||
} else {
|
||||
return (
|
||||
<Button
|
||||
type="primary"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
className="tap-ctrl tap-start"
|
||||
disabled={!this.state.query.namespace || !this.state.query.resource}
|
||||
onClick={this.props.handleTapStart}>
|
||||
|
@ -329,59 +292,143 @@ class TapQueryForm extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
renderTextInput = (title, key, helperText) => {
|
||||
let { classes } = this.props;
|
||||
return (
|
||||
<Form className="tap-form">
|
||||
<Row gutter={rowGutter}>
|
||||
<Col span={colSpan}>
|
||||
<Form.Item>
|
||||
<TextField
|
||||
id={key}
|
||||
label={title}
|
||||
className={classes.textField}
|
||||
value={this.state.query[key]}
|
||||
onChange={this.handleFormEvent(key)}
|
||||
helperText={helperText}
|
||||
margin="normal" />
|
||||
);
|
||||
}
|
||||
|
||||
renderAdvancedTapFormContent() {
|
||||
const { classes } = this.props;
|
||||
|
||||
return (
|
||||
<Grid container spacing={24}>
|
||||
<Grid item xs={6} md={3}>
|
||||
<FormControl className={classes.formControl}>
|
||||
{this.renderNamespaceSelect("To Namespace", "toNamespace", "toResource")}
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6} md={3}>
|
||||
<FormControl className={classes.formControl} disabled={_.isEmpty(this.state.query.toNamespace)}>
|
||||
{this.renderResourceSelect("toResource", "toNamespace")}
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={24}>
|
||||
<Grid item xs={6} md={3}>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor="authority">Authority</InputLabel>
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="Namespace"
|
||||
optionFilterProp="children"
|
||||
onChange={this.handleFormChange("namespace", "resource", true)}
|
||||
value={this.state.query.namespace}>
|
||||
value={this.state.query.authority}
|
||||
onChange={this.handleFormChange("authority")}
|
||||
inputProps={{ name: 'authority', id: 'authority' }}
|
||||
className={classes.selectEmpty}>
|
||||
{
|
||||
_.map(this.state.autocomplete.namespace, (n, i) => (
|
||||
<Select.Option key={`ns-dr-${i}`} value={n}>{n}</Select.Option>
|
||||
_.map(this.state.autocomplete.authority, (d, i) => (
|
||||
<MenuItem key={`authority-${i}`} value={d}>{d}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<FormHelperText>Display requests with this :authority</FormHelperText>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Col span={colSpan}>
|
||||
<Form.Item>
|
||||
{this.renderResourceSelect("resource", "namespace")}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Grid item xs={6} md={3}>
|
||||
{ this.renderTextInput("Path", "path", "Display requests with paths that start with this prefix") }
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Col span={colSpan}>
|
||||
<Form.Item>
|
||||
<Grid container spacing={24}>
|
||||
<Grid item xs={6} md={3}>
|
||||
{ this.renderTextInput("Scheme", "scheme", "Display requests with this scheme") }
|
||||
</Grid>
|
||||
<Grid item xs={6} md={3}>
|
||||
{ this.renderTextInput("Max RPS", "maxRps", `Maximum requests per second to tap. Default ${defaultMaxRps}`) }
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6} md={3}>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor="method">HTTP method</InputLabel>
|
||||
<Select
|
||||
value={this.state.query.method}
|
||||
onChange={this.handleFormChange("method")}
|
||||
inputProps={{ name: 'method', id: 'method' }}
|
||||
className={classes.selectEmpty}>
|
||||
{
|
||||
_.map(httpMethods, d => (
|
||||
<MenuItem key={`method-${d}`} value={d}>{d}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
<FormHelperText>Display requests with this HTTP method</FormHelperText>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
renderAdvancedTapForm() {
|
||||
return (
|
||||
<ExpansionPanel expanded={this.state.advancedFormExpanded} onChange={this.handleAdvancedFormExpandClick}>
|
||||
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="caption" gutterBottom>
|
||||
{this.state.advancedFormExpanded ? "Hide filters" : "Show more filters"}
|
||||
</Typography>
|
||||
</ExpansionPanelSummary>
|
||||
|
||||
<ExpansionPanelDetails>
|
||||
{this.renderAdvancedTapFormContent()}
|
||||
</ExpansionPanelDetails>
|
||||
</ExpansionPanel>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
|
||||
return (
|
||||
<Card className={classes.card}>
|
||||
<CardContent>
|
||||
<Grid container spacing={24}>
|
||||
<Grid item xs={6} md={3}>
|
||||
<FormControl className={classes.formControl}>
|
||||
{this.renderNamespaceSelect("Namespace", "namespace", "resource")}
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={6} md={3}>
|
||||
<FormControl className={classes.formControl} disabled={_.isEmpty(this.state.query.namespace)}>
|
||||
{this.renderResourceSelect("resource", "namespace")}
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={4} md={1}>
|
||||
{ this.renderTapButton(this.props.tapRequestInProgress, this.props.tapIsClosing) }
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={4} md={1}>
|
||||
<Button onClick={this.resetTapForm} disabled={this.props.tapRequestInProgress}>Reset</Button>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
|
||||
{
|
||||
!this.props.enableAdvancedForm ? null :
|
||||
<React.Fragment>
|
||||
<Button
|
||||
className="tap-form-toggle"
|
||||
onClick={() => this.toggleAdvancedForm(!this.state.showAdvancedForm)}>
|
||||
{
|
||||
this.state.showAdvancedForm ? "Hide filters" : "Show more request filters"
|
||||
} <Icon type={this.state.showAdvancedForm ? 'up' : 'down'} />
|
||||
</Button>
|
||||
<TapQueryCliCmd cmdName="tap" query={this.state.query} />
|
||||
|
||||
{ !this.state.showAdvancedForm ? null : this.renderAdvancedTapForm() }
|
||||
</React.Fragment>
|
||||
}
|
||||
</Form>
|
||||
{ !this.props.enableAdvancedForm ? null : this.renderAdvancedTapForm() }
|
||||
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default addUrlProps({ urlPropsQueryConfig })(TapQueryForm);
|
||||
export default addUrlProps({ urlPropsQueryConfig })(withStyles(styles)(TapQueryForm));
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import _ from 'lodash';
|
||||
import { emptyTapQuery } from './util/TapUtils.jsx';
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TapQueryCliCmd from './TapQueryCliCmd.jsx';
|
||||
import TapQueryForm from './TapQueryForm.jsx';
|
||||
import TopModule from './TopModule.jsx';
|
||||
import _ from 'lodash';
|
||||
import { emptyTapQuery } from './util/TapUtils.jsx';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import './../../css/tap.css';
|
||||
|
||||
class Top extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -167,7 +165,6 @@ class Top extends React.Component {
|
|||
updateQuery={this.updateQuery}
|
||||
query={this.state.query} />
|
||||
|
||||
<TapQueryCliCmd cmdName="top" query={this.state.query} />
|
||||
<TopModule
|
||||
pathPrefix={this.props.pathPrefix}
|
||||
query={this.state.query}
|
||||
|
|
|
@ -1,82 +1,68 @@
|
|||
import { directionColumn, srcDstColumn, tapLink } from './util/TapUtils.jsx';
|
||||
|
||||
import BaseTable from './BaseTable.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import { formatLatencySec } from './util/Utils.js';
|
||||
import { successRateWithMiniChart } from './util/MetricUtils.jsx';
|
||||
import { Table } from 'antd';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { directionColumn, srcDstColumn, tapLink } from './util/TapUtils.jsx';
|
||||
import { formatLatencySec, numericSort } from './util/Utils.js';
|
||||
|
||||
const topMetricColWidth = "85px";
|
||||
const topColumns = (resourceType, ResourceLink, PrefixedLink) => [
|
||||
{
|
||||
title: " ",
|
||||
key: "direction",
|
||||
dataIndex: "direction",
|
||||
width: "60px",
|
||||
render: directionColumn
|
||||
render: d => directionColumn(d.direction)
|
||||
},
|
||||
{
|
||||
title: "Name",
|
||||
key: "src-dst",
|
||||
width: "180px",
|
||||
render: d => srcDstColumn(d, resourceType, ResourceLink)
|
||||
},
|
||||
{
|
||||
title: "Method",
|
||||
dataIndex: "httpMethod",
|
||||
width: "95px",
|
||||
sorter: (a, b) => a.httpMethod.localeCompare(b.httpMethod),
|
||||
key: "httpMethod",
|
||||
render: d => d.httpMethod,
|
||||
},
|
||||
{
|
||||
title: "Path",
|
||||
dataIndex: "path",
|
||||
sorter: (a, b) => a.path.localeCompare(b.path),
|
||||
key: "path",
|
||||
render: d => d.path
|
||||
},
|
||||
{
|
||||
title: "Count",
|
||||
dataIndex: "count",
|
||||
className: "numeric",
|
||||
width: topMetricColWidth,
|
||||
defaultSortOrder: "descend",
|
||||
sorter: (a, b) => numericSort(a.count, b.count),
|
||||
key: "count",
|
||||
isNumeric: true,
|
||||
render: d => d.count
|
||||
},
|
||||
{
|
||||
title: "Best",
|
||||
dataIndex: "best",
|
||||
className: "numeric",
|
||||
width: topMetricColWidth,
|
||||
sorter: (a, b) => numericSort(a.best, b.best),
|
||||
render: formatLatencySec
|
||||
key: "best",
|
||||
isNumeric: true,
|
||||
render: d => formatLatencySec(d.best)
|
||||
},
|
||||
{
|
||||
title: "Worst",
|
||||
dataIndex: "worst",
|
||||
className: "numeric",
|
||||
width: topMetricColWidth,
|
||||
sorter: (a, b) => numericSort(a.worst, b.worst),
|
||||
render: formatLatencySec
|
||||
key: "worst",
|
||||
isNumeric: true,
|
||||
render: d => formatLatencySec(d.worst)
|
||||
},
|
||||
{
|
||||
title: "Last",
|
||||
dataIndex: "last",
|
||||
className: "numeric",
|
||||
width: topMetricColWidth,
|
||||
sorter: (a, b) => numericSort(a.last, b.last),
|
||||
render: formatLatencySec
|
||||
key: "last",
|
||||
isNumeric: true,
|
||||
render: d => formatLatencySec(d.last)
|
||||
},
|
||||
{
|
||||
title: "Success Rate",
|
||||
dataIndex: "successRate",
|
||||
className: "numeric",
|
||||
width: "128px",
|
||||
sorter: (a, b) => numericSort(a.successRate.get(), b.successRate.get()),
|
||||
render: d => successRateWithMiniChart(d.get())
|
||||
key: "successRate",
|
||||
isNumeric: true,
|
||||
render: d => _.isNil(d) || !d.get ? "---" : successRateWithMiniChart(d.get())
|
||||
},
|
||||
{
|
||||
title: "Tap",
|
||||
key: "tap",
|
||||
className: "numeric",
|
||||
width: "30px",
|
||||
isNumeric: true,
|
||||
render: d => tapLink(d, resourceType, PrefixedLink)
|
||||
}
|
||||
];
|
||||
|
@ -85,27 +71,19 @@ class TopEventTable extends React.Component {
|
|||
static propTypes = {
|
||||
api: PropTypes.shape({
|
||||
PrefixedLink: PropTypes.func.isRequired,
|
||||
ResourceLink: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
resourceType: PropTypes.string.isRequired,
|
||||
tableRows: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
}
|
||||
tableRows: PropTypes.arrayOf(PropTypes.shape({}))
|
||||
};
|
||||
static defaultProps = {
|
||||
tableRows: []
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
tableRows: []
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Table
|
||||
dataSource={this.props.tableRows}
|
||||
columns={topColumns(this.props.resourceType, this.props.api.ResourceLink, this.props.api.PrefixedLink)}
|
||||
rowKey="key"
|
||||
pagination={false}
|
||||
className="top-event-table metric-table"
|
||||
size="middle" />
|
||||
);
|
||||
}
|
||||
render() {
|
||||
const { tableRows, resourceType, api } = this.props;
|
||||
let columns = topColumns(resourceType, api.ResourceLink, api.PrefixedLink);
|
||||
return <BaseTable tableRows={tableRows} tableColumns={columns} tableClassName="metric-table" />;
|
||||
}
|
||||
}
|
||||
|
||||
export default withContext(TopEventTable);
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import _ from 'lodash';
|
||||
import { processTapEvent, setMaxRps, wsCloseCodes } from './util/TapUtils.jsx';
|
||||
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import Percentage from './util/Percentage.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TopEventTable from './TopEventTable.jsx';
|
||||
import _ from 'lodash';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import { processTapEvent, setMaxRps, wsCloseCodes } from './util/TapUtils.jsx';
|
||||
|
||||
class TopModule extends React.Component {
|
||||
static propTypes = {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { apiErrorPropType } from './util/ApiHelpers.jsx';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { apiErrorPropType } from './util/ApiHelpers.jsx';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
|
||||
class Version extends React.Component {
|
||||
|
@ -52,13 +52,14 @@ class Version extends React.Component {
|
|||
|
||||
return (
|
||||
<div>
|
||||
A new version ({this.numericVersion(latestVersion)}) is available.<br />
|
||||
<Link
|
||||
to="https://versioncheck.linkerd.io/update"
|
||||
className="button primary"
|
||||
target="_blank">
|
||||
<div className="new-version-text">A new version ({this.numericVersion(latestVersion)}) is available.</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
target="_blank"
|
||||
href="https://versioncheck.linkerd.io/update">
|
||||
Update Now
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import ExpansionPanel from '@material-ui/core/ExpansionPanel';
|
||||
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
|
||||
import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
width: '100%',
|
||||
},
|
||||
heading: {
|
||||
fontSize: theme.typography.pxToRem(15),
|
||||
flexBasis: '33.33%',
|
||||
flexShrink: 0,
|
||||
},
|
||||
secondaryHeading: {
|
||||
fontSize: theme.typography.pxToRem(15),
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
});
|
||||
|
||||
class Accordion extends React.Component {
|
||||
state = {
|
||||
expanded: this.props.defaultOpenPanel || null,
|
||||
};
|
||||
|
||||
handlePanelSelect = panel => (_event, expanded) => {
|
||||
this.setState({
|
||||
expanded: expanded ? panel : false,
|
||||
});
|
||||
|
||||
if (expanded) {
|
||||
this.props.onChange(panel);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes, panels } = this.props;
|
||||
const { expanded } = this.state;
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
{
|
||||
_.map(panels, panel => (
|
||||
<ExpansionPanel
|
||||
expanded={expanded === panel.id}
|
||||
onChange={this.handlePanelSelect(panel.id)}
|
||||
key={panel.id}>
|
||||
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
|
||||
{panel.header}
|
||||
</ExpansionPanelSummary>
|
||||
<ExpansionPanelDetails>
|
||||
{panel.body || "No data present."}
|
||||
</ExpansionPanelDetails>
|
||||
</ExpansionPanel>
|
||||
)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Accordion.propTypes = {
|
||||
classes: PropTypes.shape({}).isRequired,
|
||||
defaultOpenPanel: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
panels: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
header: PropTypes.node,
|
||||
body: PropTypes.node
|
||||
}))
|
||||
};
|
||||
|
||||
Accordion.defaultProps = {
|
||||
defaultOpenPanel: null,
|
||||
panels: []
|
||||
};
|
||||
|
||||
export default withStyles(styles)(Accordion);
|
|
@ -1,8 +1,9 @@
|
|||
import _ from 'lodash';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import 'whatwg-fetch';
|
||||
import _ from 'lodash';
|
||||
|
||||
const checkFetchOk = resp => {
|
||||
if (resp.ok) {
|
||||
|
@ -132,11 +133,7 @@ const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => {
|
|||
}
|
||||
|
||||
render() {
|
||||
let prefix = pathPrefix;
|
||||
if (!_.isEmpty(this.props.deployment)) {
|
||||
prefix = prefix.replace("/web:", "/"+this.props.deployment+":");
|
||||
}
|
||||
let url = `${prefix}${this.props.to}`;
|
||||
let url = prefixLink(this.props.to, this.props.deployment);
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
@ -148,7 +145,20 @@ const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => {
|
|||
}
|
||||
}
|
||||
|
||||
const prefixLink = (to, controllerDeployment) => {
|
||||
let prefix = pathPrefix;
|
||||
if (!_.isEmpty(controllerDeployment)) { // add field for grafana deployment
|
||||
prefix = prefix.replace("/web:", "/" + controllerDeployment + ":");
|
||||
}
|
||||
|
||||
return `${prefix}${to}`;
|
||||
};
|
||||
|
||||
const generateResourceURL = r => {
|
||||
if (r.type === "namespace") {
|
||||
return "/namespaces/" + (r.namespace || r.name);
|
||||
}
|
||||
|
||||
return "/namespaces/" + r.namespace + "/" + r.type + "s/" + r.name;
|
||||
};
|
||||
|
||||
|
@ -183,6 +193,7 @@ const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => {
|
|||
getMetricsWindowDisplayText,
|
||||
urlsForResource,
|
||||
PrefixedLink,
|
||||
prefixLink,
|
||||
ResourceLink,
|
||||
setCurrentRequests,
|
||||
getCurrentPromises,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
/*
|
||||
* Instructions for adding resources to service mesh
|
||||
|
@ -6,17 +7,17 @@ import React from 'react';
|
|||
export const incompleteMeshMessage = name => {
|
||||
if (name) {
|
||||
return (
|
||||
<div className="action">
|
||||
<Typography>
|
||||
Add {name} to the k8s.yml file<br /><br />
|
||||
Then run <code>linkerd inject k8s.yml | kubectl apply -f -</code> to add it to the service mesh
|
||||
</div>
|
||||
</Typography>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="action">
|
||||
<Typography>
|
||||
Add one or more resources to the k8s.yml file<br /><br />
|
||||
Then run <code>linkerd inject k8s.yml | kubectl apply -f -</code> to add them to the service mesh
|
||||
</div>
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import _ from 'lodash';
|
||||
import { metricToFormatter } from './Utils.js';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Percentage from './Percentage.js';
|
||||
import { Progress } from 'antd';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import { metricToFormatter } from './Utils.js';
|
||||
|
||||
const getPodCategorization = pod => {
|
||||
if (pod.added && pod.status === "Running") {
|
||||
return "good";
|
||||
} else if (pod.status === "Pending" || pod.status === "Running") {
|
||||
return "neutral";
|
||||
return "default";
|
||||
} else if (pod.status === "Failed") {
|
||||
return "bad";
|
||||
return "poor";
|
||||
}
|
||||
return ""; // Terminating | Succeeded | Unknown
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ export const getSuccessRateClassification = (rate, successRateLabels) => {
|
|||
}
|
||||
|
||||
if (rate < 0.9) {
|
||||
return successRateLabels.bad;
|
||||
return successRateLabels.poor;
|
||||
} else if (rate < 0.95) {
|
||||
return successRateLabels.neutral;
|
||||
} else {
|
||||
|
@ -31,24 +31,17 @@ export const getSuccessRateClassification = (rate, successRateLabels) => {
|
|||
};
|
||||
|
||||
export const srArcClassLabels = {
|
||||
good: "status-good",
|
||||
neutral: "status-ok",
|
||||
bad: "status-poor",
|
||||
default: "status-ok"
|
||||
good: "good",
|
||||
neutral: "neutral",
|
||||
poor: "poor",
|
||||
default: "default"
|
||||
};
|
||||
|
||||
export const successRateWithMiniChart = sr => (
|
||||
<div>
|
||||
<span className="metric-table-sr">{metricToFormatter["SUCCESS_RATE"](sr)}</span>
|
||||
<Progress
|
||||
className={`success-rate-arc ${getSuccessRateClassification(sr, srArcClassLabels)} metric-table-sr-chart`}
|
||||
type="dashboard"
|
||||
showInfo={false}
|
||||
width={32}
|
||||
strokeWidth={12}
|
||||
percent={sr === 0 ? 100 : sr * 100} // if success rate is 0, we want a red chart, not a gray chart
|
||||
gapDegree={180} />
|
||||
</div>
|
||||
<Grid container spacing={8}>
|
||||
<Grid item>{metricToFormatter["SUCCESS_RATE"](sr)}</Grid>
|
||||
<Grid item>{_.isNil(sr) ? null : <div className={`success-rate-dot status-dot status-dot-${getSuccessRateClassification(sr, srArcClassLabels)}`} />}</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
const getTotalRequests = row => {
|
||||
|
@ -150,6 +143,7 @@ const processStatTable = table => {
|
|||
let runningPodCount = parseInt(row.runningPodCount, 10);
|
||||
let meshedPodCount = parseInt(row.meshedPodCount, 10);
|
||||
return {
|
||||
key: `${row.resource.namespace}-${row.resource.type}-${row.resource.name}`,
|
||||
name: row.resource.name,
|
||||
namespace: row.resource.namespace,
|
||||
type: row.resource.type,
|
||||
|
@ -210,6 +204,7 @@ export const excludeResourcesFromRollup = (rollupMetrics, resourcesToExclude) =>
|
|||
};
|
||||
|
||||
export const emptyMetric = {
|
||||
key: "",
|
||||
name: "",
|
||||
namespace: "",
|
||||
type: "",
|
||||
|
|
|
@ -17,6 +17,7 @@ describe('MetricUtils', () => {
|
|||
name: 'voting',
|
||||
namespace: 'emojivoto',
|
||||
type: 'deployment',
|
||||
key: "emojivoto-deployment-voting",
|
||||
requestRate: 2.5,
|
||||
successRate: 0.9,
|
||||
totalRequests: 150,
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import LinearProgress from '@material-ui/core/LinearProgress';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
const colorLookup = {
|
||||
good: {
|
||||
colorPrimary: '#c8e6c9', // background bar color (lighter)
|
||||
barColorPrimary: '#388e3c', // inner bar color (darker)
|
||||
},
|
||||
neutral: {
|
||||
colorPrimary: '#ffcc80',
|
||||
barColorPrimary: '#ef6c00',
|
||||
},
|
||||
poor: {
|
||||
colorPrimary: '#ffebee',
|
||||
barColorPrimary: '#d32f2f',
|
||||
},
|
||||
default: {
|
||||
colorPrimary: '#e8eaf6',
|
||||
barColorPrimary: '#3f51b5',
|
||||
}
|
||||
};
|
||||
|
||||
export const StyledProgress = (classification = "default") => withStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
colorPrimary: {
|
||||
backgroundColor: colorLookup[classification].colorPrimary,
|
||||
},
|
||||
barColorPrimary: {
|
||||
backgroundColor: colorLookup[classification].barColorPrimary,
|
||||
},
|
||||
})(LinearProgress);
|
|
@ -0,0 +1,28 @@
|
|||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
const styles = () => ({
|
||||
progress: {
|
||||
"margin": "auto",
|
||||
},
|
||||
});
|
||||
|
||||
function CircularIndeterminate(props) {
|
||||
const { classes } = props;
|
||||
return (
|
||||
<Grid container justify="center">
|
||||
<div >
|
||||
<CircularProgress className={classes.progress} style={{ color: "#26E99D" }} />
|
||||
</div>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
CircularIndeterminate.propTypes = {
|
||||
classes: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
export default withStyles(styles)(CircularIndeterminate);
|
|
@ -1,6 +1,19 @@
|
|||
import React from 'react';
|
||||
import './../../../css/svg-wrappers.css';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export const slackIcon = (
|
||||
<svg style={{"width":"24px", "height":"24px"}} viewBox="0 0 24 24">
|
||||
<path fill="#757575" d="M10.23,11.16L12.91,10.27L13.77,12.84L11.09,13.73L10.23,11.16M17.69,13.71C18.23,13.53 18.5,12.94 18.34,12.4C18.16,11.86 17.57,11.56 17.03,11.75L15.73,12.18L14.87,9.61L16.17,9.17C16.71,9 17,8.4 16.82,7.86C16.64,7.32 16.05,7 15.5,7.21L14.21,7.64L13.76,6.3C13.58,5.76 13,5.46 12.45,5.65C11.91,5.83 11.62,6.42 11.8,6.96L12.25,8.3L9.57,9.19L9.12,7.85C8.94,7.31 8.36,7 7.81,7.2C7.27,7.38 7,7.97 7.16,8.5L7.61,9.85L6.31,10.29C5.77,10.47 5.5,11.06 5.66,11.6C5.8,12 6.19,12.3 6.61,12.31L6.97,12.25L8.27,11.82L9.13,14.39L7.83,14.83C7.29,15 7,15.6 7.18,16.14C7.32,16.56 7.71,16.84 8.13,16.85L8.5,16.79L9.79,16.36L10.24,17.7C10.38,18.13 10.77,18.4 11.19,18.41L11.55,18.35C12.09,18.17 12.38,17.59 12.2,17.04L11.75,15.7L14.43,14.81L14.88,16.15C15,16.57 15.41,16.84 15.83,16.85L16.19,16.8C16.73,16.62 17,16.03 16.84,15.5L16.39,14.15L17.69,13.71M21.17,9.25C23.23,16.12 21.62,19.1 14.75,21.17C7.88,23.23 4.9,21.62 2.83,14.75C0.77,7.88 2.38,4.9 9.25,2.83C16.12,0.77 19.1,2.38 21.17,9.25Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const githubIcon = (
|
||||
<svg style={{"width":"24px", "height":"24px"}} viewBox="0 0 24 24">
|
||||
<path fill="#757575" d="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const grafanaIcon = (
|
||||
<svg
|
||||
version="1.1"
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import _ from 'lodash';
|
||||
import { podOwnerLookup, toShortResourceName } from './Utils.js';
|
||||
|
||||
import BaseTable from '../BaseTable.jsx';
|
||||
import Popover from '../Popover.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TapLink from '../TapLink.jsx';
|
||||
import { podOwnerLookup, toShortResourceName } from './Utils.js';
|
||||
import { Popover, Table, Tooltip } from 'antd';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import _ from 'lodash';
|
||||
|
||||
export const httpMethods = ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"];
|
||||
|
||||
|
@ -192,9 +195,9 @@ const resourceShortLink = (resourceType, labels, ResourceLink) => (
|
|||
|
||||
const displayLimit = 3; // how many upstreams/downstreams to display in the popover table
|
||||
const popoverSrcDstColumns = [
|
||||
{ title: "Source", dataIndex: "source", key: "source" },
|
||||
{ title: "", render: () => <i className="fas fa-long-arrow-alt-right" /> },
|
||||
{ title: "Destination", dataIndex: "destination", key: "destination" }
|
||||
{ title: "Source", key: "source", render: d => d.source },
|
||||
{ title: "", key: "arrow", render: () => <i className="fas fa-long-arrow-alt-right" /> },
|
||||
{ title: "Destination", key: "destination", render: d => d.destination }
|
||||
];
|
||||
|
||||
const getPodOwner = (labels, ResourceLink) => {
|
||||
|
@ -266,11 +269,10 @@ const popoverResourceTable = (d, ResourceLink) => {
|
|||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={popoverSrcDstColumns}
|
||||
dataSource={tableData}
|
||||
className="metric-table"
|
||||
pagination={false} />
|
||||
<BaseTable
|
||||
tableColumns={popoverSrcDstColumns}
|
||||
tableRows={tableData}
|
||||
tableClassName="metric-table" />
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -285,10 +287,8 @@ export const extractPodOwner = labels => {
|
|||
};
|
||||
|
||||
export const directionColumn = d => (
|
||||
<Tooltip
|
||||
title={d}
|
||||
overlayStyle={{ fontSize: "12px" }}>
|
||||
{d === "INBOUND" ? "FROM" : "TO"}
|
||||
<Tooltip title={d} placement="right">
|
||||
<span>{d === "INBOUND" ? "FROM" : "TO"}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
|
@ -304,14 +304,16 @@ export const srcDstColumn = (d, resourceType, ResourceLink) => {
|
|||
labels = d.destinationLabels;
|
||||
}
|
||||
|
||||
let baseContent = (
|
||||
<div className="src-dst-name">
|
||||
{ !_.isEmpty(labels[resourceType]) ? resourceShortLink(resourceType, labels, ResourceLink) : display.str }
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={popoverResourceTable(d, ResourceLink)}
|
||||
trigger="hover">
|
||||
<div className="src-dst-name">
|
||||
{ !_.isEmpty(labels[resourceType]) ? resourceShortLink(resourceType, labels, ResourceLink) : display.str }
|
||||
</div>
|
||||
</Popover>
|
||||
popoverContent={popoverResourceTable(d, ResourceLink)}
|
||||
baseContent={baseContent} />
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import _ from 'lodash';
|
||||
import * as d3 from 'd3';
|
||||
import _ from 'lodash';
|
||||
|
||||
/*
|
||||
* Display grid constants
|
||||
|
@ -111,11 +111,6 @@ export const toClassName = name => {
|
|||
return _.lowerCase(name).replace(/[^a-zA-Z0-9]/g, "_");
|
||||
};
|
||||
|
||||
/*
|
||||
Definition of sort, for ant table sorting
|
||||
*/
|
||||
export const numericSort = (a, b) => (_.isNil(a) ? -1 : a) - (_.isNil(b) ? -1 : b);
|
||||
|
||||
/*
|
||||
Nicely readable names for the stat resources
|
||||
*/
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import { withContext } from './AppContext.jsx';
|
||||
|
||||
/**
|
||||
|
@ -44,10 +44,10 @@ const withREST = (WrappedComponent, componentPromises, options={}) => {
|
|||
this.startServerPolling(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
componentDidUpdate(prevProps) {
|
||||
const changed = _.filter(
|
||||
localOptions.resetProps,
|
||||
prop => _.get(newProps, prop) !== _.get(this.props, prop),
|
||||
prop => _.get(prevProps, prop) !== _.get(this.props, prop),
|
||||
);
|
||||
|
||||
if (_.isEmpty(changed)) { return; }
|
||||
|
@ -55,7 +55,7 @@ const withREST = (WrappedComponent, componentPromises, options={}) => {
|
|||
// React won't unmount this component when switching resource pages so we need to clear state
|
||||
this.stopServerPolling();
|
||||
this.setState(this.getInitialState());
|
||||
this.startServerPolling(newProps);
|
||||
this.startServerPolling(this.props);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
import './../css/styles.css';
|
||||
import './../img/favicon.png'; // needs to be referenced somewhere so webpack bundles it
|
||||
|
||||
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom';
|
||||
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
|
||||
|
||||
import ApiHelpers from './components/util/ApiHelpers.jsx';
|
||||
import AppContext from './components/util/AppContext.jsx';
|
||||
import BreadcrumbHeader from './components/BreadcrumbHeader.jsx';
|
||||
import { Layout } from 'antd';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import Namespace from './components/Namespace.jsx';
|
||||
import NamespaceLanding from './components/NamespaceLanding.jsx';
|
||||
import Navigation from './components/Navigation.jsx';
|
||||
import NoMatch from './components/NoMatch.jsx';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
@ -11,12 +17,9 @@ import ResourceDetail from './components/ResourceDetail.jsx';
|
|||
import ResourceList from './components/ResourceList.jsx';
|
||||
import { RouterToUrlQuery } from 'react-url-query';
|
||||
import ServiceMesh from './components/ServiceMesh.jsx';
|
||||
import Sidebar from './components/Sidebar.jsx';
|
||||
import Tap from './components/Tap.jsx';
|
||||
import Top from './components/Top.jsx';
|
||||
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom';
|
||||
import './../css/styles.css';
|
||||
import './../img/favicon.png'; // needs to be referenced somewhere so webpack bundles it
|
||||
import green from '@material-ui/core/colors/green';
|
||||
|
||||
let appMain = document.getElementById('main');
|
||||
let appData = !appMain ? {} : appMain.dataset;
|
||||
|
@ -34,50 +37,72 @@ const context = {
|
|||
productName: "Linkerd"
|
||||
};
|
||||
|
||||
const theme = createMuiTheme({
|
||||
palette: {
|
||||
primary: green
|
||||
},
|
||||
typography: {
|
||||
useNextVariants: true,
|
||||
suppressDeprecationWarnings: true // https://github.com/mui-org/material-ui/issues/13175
|
||||
}
|
||||
});
|
||||
|
||||
let applicationHtml = (
|
||||
<AppContext.Provider value={context}>
|
||||
<BrowserRouter>
|
||||
<RouterToUrlQuery>
|
||||
<Layout>
|
||||
<Route component={Sidebar} />
|
||||
<Layout>
|
||||
<Route component={BreadcrumbHeader} />
|
||||
<Layout.Content style={{ margin: '60px 0', padding: 0, background: '#fff' }}>
|
||||
<div className="main-content">
|
||||
<Switch>
|
||||
<Redirect exact from={`${pathPrefix}/`} to={`${pathPrefix}/overview`} />
|
||||
<Route path={`${pathPrefix}/overview`} component={NamespaceLanding} />
|
||||
<Route path={`${pathPrefix}/servicemesh`} component={ServiceMesh} />
|
||||
<Route exact path={`${pathPrefix}/namespaces/:namespace`} component={Namespace} />
|
||||
<Route path={`${pathPrefix}/namespaces/:namespace/pods/:pod`} component={ResourceDetail} />
|
||||
<Route path={`${pathPrefix}/namespaces/:namespace/deployments/:deployment`} component={ResourceDetail} />
|
||||
<Route path={`${pathPrefix}/namespaces/:namespace/replicationcontrollers/:replicationcontroller`} component={ResourceDetail} />
|
||||
<Route path={`${pathPrefix}/tap`} component={Tap} />
|
||||
<Route path={`${pathPrefix}/top`} component={Top} />
|
||||
<Route
|
||||
path={`${pathPrefix}/namespaces`}
|
||||
render={() => <ResourceList resource="namespace" />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/deployments`}
|
||||
render={() => <ResourceList resource="deployment" />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/replicationcontrollers`}
|
||||
render={() => <ResourceList resource="replicationcontroller" />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/pods`}
|
||||
render={() => <ResourceList resource="pod" />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/authorities`}
|
||||
render={() => <ResourceList resource="authority" />} />
|
||||
<Route component={NoMatch} />
|
||||
</Switch>
|
||||
</div>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</RouterToUrlQuery>
|
||||
</BrowserRouter>
|
||||
</AppContext.Provider>
|
||||
<React.Fragment>
|
||||
<CssBaseline />
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<AppContext.Provider value={context}>
|
||||
<BrowserRouter>
|
||||
<RouterToUrlQuery>
|
||||
<Switch>
|
||||
<Redirect exact from={`${pathPrefix}/`} to={`${pathPrefix}/overview`} />
|
||||
<Route
|
||||
path={`${pathPrefix}/overview`}
|
||||
render={props => <Navigation {...props} ChildComponent={NamespaceLanding} />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/servicemesh`}
|
||||
render={props => <Navigation {...props} ChildComponent={ServiceMesh} />} />
|
||||
<Route
|
||||
exact
|
||||
path={`${pathPrefix}/namespaces/:namespace`}
|
||||
render={props => <Navigation {...props} ChildComponent={Namespace} />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/namespaces/:namespace/pods/:pod`}
|
||||
render={props => <Navigation {...props} ChildComponent={ResourceDetail} />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/namespaces/:namespace/deployments/:deployment`}
|
||||
render={props => <Navigation {...props} ChildComponent={ResourceDetail} />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/namespaces/:namespace/replicationcontrollers/:replicationcontroller`}
|
||||
render={props => <Navigation {...props} ChildComponent={ResourceDetail} />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/tap`}
|
||||
render={props => <Navigation {...props} ChildComponent={Tap} />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/top`}
|
||||
render={props => <Navigation {...props} ChildComponent={Top} />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/namespaces`}
|
||||
render={props => <Navigation {...props} ChildComponent={ResourceList} resource="namespace" />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/deployments`}
|
||||
render={props => <Navigation {...props} ChildComponent={ResourceList} resource="deployment" />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/replicationcontrollers`}
|
||||
render={props => <Navigation {...props} ChildComponent={ResourceList} resource="replicationcontroller" />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/pods`}
|
||||
render={props => <Navigation {...props} ChildComponent={ResourceList} resource="pod" />} />
|
||||
<Route
|
||||
path={`${pathPrefix}/authorities`}
|
||||
render={props => <Navigation {...props} ChildComponent={ResourceList} resource="authority" />} />
|
||||
<Route component={NoMatch} />
|
||||
</Switch>
|
||||
</RouterToUrlQuery>
|
||||
</BrowserRouter>
|
||||
</AppContext.Provider>
|
||||
</MuiThemeProvider>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
ReactDOM.render(applicationHtml, appMain);
|
||||
|
|
|
@ -4,58 +4,61 @@
|
|||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"antd": "^3.7.2",
|
||||
"d3": "^4.11.0",
|
||||
"lodash": "^4.17.10",
|
||||
"moment": "^2.18.1",
|
||||
"path": "^0.12.7",
|
||||
"prop-types": "^15.6.1",
|
||||
"react": "^16.5.0",
|
||||
"react-dom": "^16.5.0",
|
||||
"react-router": "^4.2.0",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-router-prop-types": "^1.0.3",
|
||||
"react-url-query": "^1.4.0",
|
||||
"whatwg-fetch": "^2.0.3"
|
||||
"@material-ui/core": "3.2.2",
|
||||
"@material-ui/icons": "3.0.1",
|
||||
"classnames": "2.2.6",
|
||||
"d3": "4.11.0",
|
||||
"lodash": "4.17.10",
|
||||
"moment": "2.18.1",
|
||||
"path": "0.12.7",
|
||||
"prop-types": "15.6.1",
|
||||
"react": "16.5.0",
|
||||
"react-dom": "16.5.0",
|
||||
"react-router": "4.2.0",
|
||||
"react-router-dom": "4.2.2",
|
||||
"react-router-prop-types": "^1.0.4",
|
||||
"react-url-query": "1.4.0",
|
||||
"whatwg-fetch": "2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.0.3",
|
||||
"babel-jest": "^23.6.0",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-plugin-import": "^1.7.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-react-app": "^3.1.1",
|
||||
"css-loader": "^0.28.7",
|
||||
"enzyme": "^3.7.0",
|
||||
"enzyme-adapter-react-16": "^1.6.0",
|
||||
"eslint": "^4.12.1",
|
||||
"eslint-config-airbnb": "^16.1.0",
|
||||
"eslint-loader": "^2.0.0",
|
||||
"eslint-plugin-import": "^2.12.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.0.3",
|
||||
"eslint-plugin-promise": "^3.6.0",
|
||||
"eslint-plugin-react": "^7.5.1",
|
||||
"file-loader": "^1.1.5",
|
||||
"history": "^4.7.2",
|
||||
"jest": "^23.6.0",
|
||||
"jest-dot-reporter": "^1.0.7",
|
||||
"jest-enzyme": "^7.0.0",
|
||||
"less": "^3.8.1",
|
||||
"less-loader": "^4.1.0",
|
||||
"less-vars-to-js": "^1.3.0",
|
||||
"postcss": "^6.0.22",
|
||||
"postcss-cssnext": "^3.1.0",
|
||||
"postcss-import": "^11.1.0",
|
||||
"postcss-loader": "^2.1.5",
|
||||
"react-test-renderer": "^16.5.2",
|
||||
"sinon": "^7.0.0",
|
||||
"sinon-stub-promise": "^4.0.0",
|
||||
"style-loader": "^0.21.0",
|
||||
"url-loader": "^1.0.1",
|
||||
"webpack": "^4.20.2",
|
||||
"webpack-cli": "^3.0.7",
|
||||
"webpack-dev-server": "^3.1.9"
|
||||
"babel-core": "6.26.0",
|
||||
"babel-eslint": "8.0.3",
|
||||
"babel-jest": "23.6.0",
|
||||
"babel-loader": "7.1.4",
|
||||
"babel-plugin-import": "1.7.0",
|
||||
"babel-preset-env": "1.7.0",
|
||||
"babel-preset-react-app": "3.1.1",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"css-loader": "0.28.7",
|
||||
"enzyme": "3.7.0",
|
||||
"enzyme-adapter-react-16": "1.6.0",
|
||||
"eslint": "4.12.1",
|
||||
"eslint-config-airbnb": "16.1.0",
|
||||
"eslint-loader": "2.0.0",
|
||||
"eslint-plugin-import": "2.12.0",
|
||||
"eslint-plugin-jsx-a11y": "6.0.3",
|
||||
"eslint-plugin-promise": "3.6.0",
|
||||
"eslint-plugin-react": "7.11.1",
|
||||
"file-loader": "2.0.0",
|
||||
"history": "4.7.2",
|
||||
"jest": "23.6.0",
|
||||
"jest-dot-reporter": "1.0.7",
|
||||
"jest-enzyme": "7.0.0",
|
||||
"less": "3.8.1",
|
||||
"less-loader": "4.1.0",
|
||||
"less-vars-to-js": "1.3.0",
|
||||
"postcss": "6.0.22",
|
||||
"postcss-cssnext": "3.1.0",
|
||||
"postcss-import": "11.1.0",
|
||||
"postcss-loader": "2.1.5",
|
||||
"react-test-renderer": "16.5.2",
|
||||
"sinon": "7.0.0",
|
||||
"sinon-stub-promise": "4.0.0",
|
||||
"style-loader": "0.21.0",
|
||||
"url-loader": "1.0.1",
|
||||
"webpack": "4.20.2",
|
||||
"webpack-cli": "3.0.7",
|
||||
"webpack-dev-server": "3.1.9"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "enzyme",
|
||||
|
|
|
@ -62,7 +62,7 @@ module.exports = {
|
|||
loader: 'less-loader',
|
||||
options: {
|
||||
modifyVars: {
|
||||
'font-family': '\'Lato\', helvetica, arial, sans-serif'
|
||||
'font-family': '\'Roboto\', \'Lato\', helvetica, arial, sans-serif'
|
||||
},
|
||||
javascriptEnabled: true
|
||||
}
|
||||
|
|
2710
web/app/yarn.lock
2710
web/app/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue