Skip to content

Commit

Permalink
refactor(dashboard): use a single generic node info container
Browse files Browse the repository at this point in the history
As a side effect, all nodes now have a info pane. Also added a refresh
button to the pane and made minor style changes.
  • Loading branch information
eysi09 committed May 6, 2019
1 parent 54d6cd2 commit 09d3d58
Show file tree
Hide file tree
Showing 9 changed files with 356 additions and 485 deletions.
13 changes: 5 additions & 8 deletions dashboard/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import cls from "classnames"
import { css } from "emotion/macro"
import React, { useContext } from "react"
import styled from "@emotion/styled/macro"
Expand Down Expand Up @@ -112,13 +111,11 @@ const App = () => {
`}
>
<div
className={cls(
css`
background-color: ${colors.grayLight}
flex-grow: 1;
padding: 1rem 1rem 1rem 3rem;
`,
)}
className={css`
background-color: ${colors.grayLight}
flex-grow: 1;
padding: 1rem 1rem 1rem 3rem;
`}
>
<Route exact path="/" component={Overview} />
<Route path="/logs/" component={Logs} />
Expand Down
59 changes: 59 additions & 0 deletions dashboard/src/components/RefreshButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2018 Garden Technologies, Inc. <info@garden.io>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import styled from "@emotion/styled/macro"
import React from "react"

import { colors } from "../styles/variables"

interface Props {
onClick: () => void
loading: boolean
}

const Button = styled.div`
padding: 0.3em;
border-radius: 10%;
cursor: pointer;
:active {
opacity: 0.5;
}
`

const Icon = styled.i`
color: ${colors.gardenGray};
font-size: 1.25rem;
:hover {
color: ${colors.gardenPink}
}
:active {
opacity: 0.5;
}
`

const IconLoading = styled(Icon)`
animation spin 0.5s infinite linear;
@keyframes spin {
from {
transform:rotate(0deg);
}
to {
transform:rotate(360deg);
}
}
`

export const RefreshButton: React.FC<Props> = ({ loading, onClick }) => {
const IconComp = loading ? IconLoading : Icon

return (
<Button onClick={onClick}>
<IconComp className={"fas fa-redo-alt"} />
</Button>
)
}
7 changes: 2 additions & 5 deletions dashboard/src/components/graph/graph.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
}

.node:hover rect, .node.selected rect {
stroke-width: 4.5px;
stroke-width: 4.5px;
}

.edgePath path {
Expand All @@ -52,11 +52,8 @@
background-size: 4rem;
border-radius: 5px;
border:none;
cursor: pointer;

&--run,
&--test {
cursor: pointer;
}
.type {
color: gray;
padding-bottom: 0.2rem;
Expand Down
186 changes: 186 additions & 0 deletions dashboard/src/components/info-pane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
* Copyright (C) 2018 Garden Technologies, Inc. <info@garden.io>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import React from "react"
import cls from "classnames"
import { capitalize } from "lodash"
import { css } from "emotion/macro"
import styled from "@emotion/styled/macro"
import Card from "../components/card"
import { colors } from "../styles/variables"
import { RenderedNode } from "garden-cli/src/config-graph"
import { RefreshButton } from "./RefreshButton"

export const ErrorTxt = styled.div`
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
position: relative;
padding: 0.75rem 1.25rem;
border: 1px solid transparent;
border-radius: 0.25rem;
`

const Term = styled.div`
background-color: ${colors.gardenBlack};
color: white;
border-radius: 2px;
max-height: 45rem;
overflow-y: auto;
padding: 1rem;
`
const Code = styled.code`
font-size: 0.8rem;
white-space: pre-wrap;
`

const ClosePaneContainer = styled.div`
display: flex;
margin-left: auto;
`
const ClosePane = styled.div`
cursor: pointer;
background-size: contain;
width: 2rem;
height: 2rem;
`

const IconContainer = styled.span`
display: inline-block;
width: 2rem;
height: 2rem;
background-size: contain;
vertical-align: text-top;
background-repeat: no-repeat;
vertical-align: top;
`
interface Props {
node: RenderedNode
clearGraphNodeSelection: () => void
onRefresh?: () => void
loading?: boolean
output?: string | null
startedAt?: string | null
completedAt?: string | null
duration?: string | null
}

const Key = ({ text }) => (
<div
className={cls(css`
font-weight: bold;
`,
"col-xs-5 col-lg-3 pr-1")}
>
{text}
</div>
)

// TODO: Split up into something InfoPane and InfoPaneWithResults. Props are kind of messy.
export const InfoPane: React.FC<Props> = ({
clearGraphNodeSelection,
loading,
onRefresh,
node,
output,
startedAt,
completedAt,
duration,
}) => {
const { name, moduleName, type } = node
let outputEl: React.ReactNode = null

if (output) {
outputEl = (
<Term>
<Code>{output}</Code>
</Term>
)
} else if (output === null) {
// Output explictly set to null means that the data was fetched but the result was empty
outputEl = <ErrorTxt>No test output</ErrorTxt>
}

return (
<Card>
<div className="p-1">
<div className="row">
<div>
<IconContainer className={cls(`garden-icon`, `garden-icon--${type}`)} />
</div>
<div
className={css`
padding-left: 0.5rem;
`}
>
<h2
className={css`
margin-block-end: 0;
`}
>
{name}
</h2>
</div>

<ClosePaneContainer>
{onRefresh && (
<div className={css`margin-right: 1rem;`}>
<RefreshButton onClick={onRefresh} loading={loading || false} />
</div>
)}
<ClosePane
onClick={clearGraphNodeSelection}
className="garden-icon garden-icon--close"
/>
</ClosePaneContainer>
</div>

<div className="row pt-2">
<Key text="Type" />
<div className="col-xs col-lg">
{capitalize(type)}
</div>
</div>

<div className="row pt-1">
<Key text="Module" />
<div className="col-xs col-lg">{moduleName}</div>
</div>

{duration && (
<div className="row pt-1">
<Key text="Duration" />
<div className="col-xs col-lg">{duration}</div>
</div>
)}

{startedAt && (
<div className="row pt-1">
<Key text="Started At" />
<div className="col-xs col-lg">{startedAt}</div>
</div>
)}

{completedAt && (
<div className="row pt-1">
<Key text="Completed At" />
<div className="col-xs col-lg">{completedAt}</div>
</div>
)}

{(type === "test" || type === "run") && (
<div className="row pt-1">
<div className="col-xs-12">
{outputEl}
</div>
</div>
)}
</div>
</Card>
)
}
41 changes: 2 additions & 39 deletions dashboard/src/components/logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { getServiceNames } from "../util/helpers"

import { ServiceLogEntry } from "garden-cli/src/types/plugin/outputs"
import { ConfigDump } from "garden-cli/src/garden"
import { RefreshButton } from "./RefreshButton"

interface Props {
config: ConfigDump
Expand All @@ -39,40 +40,6 @@ const Header = styled.div`
align-items: center;
`

const Button = styled.div`
padding: 0.3em;
border-radius: 10%;
border: 2px solid ${colors.gardenGrayLight};
cursor: pointer;
:hover {
border: 2px solid ${colors.gardenGray};
transition: all 0.3s ease-out;
}
:active {
opacity: 0.5;
}
`

const Icon = styled.i`
color: ${colors.gardenGray};
font-size: 1.25rem;
:active {
opacity: 0.5;
}
`

const IconLoading = styled(Icon)`
animation spin 0.5s infinite linear;
@keyframes spin {
from {
transform:rotate(0deg);
}
to {
transform:rotate(360deg);
}
}
`

// TODO: Roll our own Select component instead of using react-select, it's an overkill.
const selectStyles = {
control: (base, state) => ({
Expand Down Expand Up @@ -133,8 +100,6 @@ class Logs extends Component<Props, State> {
const title = value === "all" ? label : `${label} logs`
const filteredLogs = value === "all" ? logs : logs.filter(l => l.serviceName === value)

const IconComp = loading ? IconLoading : Icon

return (
<div>
<div
Expand All @@ -154,9 +119,7 @@ class Logs extends Component<Props, State> {
<div>
<Header className="p-1">
<CardTitle>{title}</CardTitle>
<Button onClick={this.refresh}>
<IconComp className={"fas fa-redo-alt"} />
</Button>
<RefreshButton onClick={this.refresh} loading={loading} />
</Header>
<Terminal
entries={filteredLogs}
Expand Down
17 changes: 2 additions & 15 deletions dashboard/src/containers/graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import { EventContext } from "../context/events"
import LoadWrapper from "../components/load-wrapper"
import { DataContext } from "../context/data"
import { UiStateContext } from "../context/ui"
import { TaskResultNodeInfo } from "./task-result-node-info"
import { TestResultNodeInfo } from "./test-result-node-info"
import { NodeInfo } from "./node-info"

export default () => {
const {
Expand All @@ -39,19 +38,7 @@ export default () => {
if (selectedGraphNode && graph.data) {
const node = graph.data.nodes.find(n => n.key === selectedGraphNode)
if (node) {
const { name, type, moduleName } = node
switch (type) {
case "run": // task
moreInfoPane = <TaskResultNodeInfo name={name} />
break
case "test":
moreInfoPane = <TestResultNodeInfo name={name} module={moduleName} />
break
case "build":
default:
moreInfoPane = null
break
}
moreInfoPane = <NodeInfo node={node} />
}
}

Expand Down
Loading

0 comments on commit 09d3d58

Please sign in to comment.