diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 9b2d1e18cb..5b538b281c 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -12560,6 +12560,11 @@ } } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index 98c24beb62..265da27ed4 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -31,6 +31,7 @@ "flexboxgrid-helpers": "^1.1.3", "http-proxy-middleware": "^0.19.1", "lodash": "^4.17.11", + "moment": "^2.24.0", "node-sass": "^4.10.0", "normalize-url": "^4.3.0", "react": "^16.8.6", diff --git a/dashboard/src/api/api.ts b/dashboard/src/api/api.ts index 5a29b06668..c7083f537f 100644 --- a/dashboard/src/api/api.ts +++ b/dashboard/src/api/api.ts @@ -35,7 +35,7 @@ export async function fetchGraph() { } export async function fetchStatus() { - return apiPost("get.status") + return apiPost("get.status", { output: "json" }) } export async function fetchLogs(services: FetchLogsParam) { diff --git a/dashboard/src/app.tsx b/dashboard/src/app.tsx index 40745b6ac5..6fd25cee3d 100644 --- a/dashboard/src/app.tsx +++ b/dashboard/src/app.tsx @@ -41,10 +41,10 @@ const Logo = styled.img` ` const SidebarWrapper = styled.div` - border-right: 1px solid ${colors.border}; height: 100vh; position: relative; background: ${colors.gardenWhite}; + box-shadow: 6px 0px 18px rgba(0, 0, 0, 0.06); ` type SidebarContainerProps = { @@ -61,7 +61,7 @@ const SidebarToggleButton = styled.div` top: 2rem; width: 1.5rem; cursor: pointer; - font-size: 1.25rem; + font-size: 1.125rem; ` const AppContainer = () => { @@ -112,14 +112,14 @@ const App = () => { display: flex; flex-direction: column; flex-grow: 1; - overflow-y: auto; + overflow-y: hidden; `} >
diff --git a/dashboard/src/assets/close-pane.svg b/dashboard/src/assets/close-pane.svg index 3582863c5f..86a23846cf 100644 --- a/dashboard/src/assets/close-pane.svg +++ b/dashboard/src/assets/close-pane.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/dashboard/src/components/ActionIcon.tsx b/dashboard/src/components/ActionIcon.tsx index 384e57bc0a..6f11d6d0cf 100644 --- a/dashboard/src/components/ActionIcon.tsx +++ b/dashboard/src/components/ActionIcon.tsx @@ -18,9 +18,11 @@ interface Props { } const Button = styled.div` - border-radius: 10%; + border-radius: 4px; margin: .5rem; cursor: pointer; + display: flex; + align-items: center; :active { opacity: 0.5; } diff --git a/dashboard/src/components/card.tsx b/dashboard/src/components/card.tsx index 6d35968a35..32b409a3c9 100644 --- a/dashboard/src/components/card.tsx +++ b/dashboard/src/components/card.tsx @@ -23,8 +23,8 @@ type WrapperProps = { const Wrapper = styled.div` background-color: ${props => props.backgroundColor || colors.gardenWhite}; - border-radius: 2px; - box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); + box-shadow: 0px 6px 18px rgba(0, 0, 0, 0.06); + border-radius: 4px; width: 100%; overflow: hidden; ` diff --git a/dashboard/src/components/graph/graph.scss b/dashboard/src/components/graph/graph.scss index e91b1254f0..ad4a0c2aa5 100644 --- a/dashboard/src/components/graph/graph.scss +++ b/dashboard/src/components/graph/graph.scss @@ -50,7 +50,7 @@ background-position: left; background-position-x: -0.5rem; background-size: 4rem; - border-radius: 5px; + border-radius: 4px; border:none; cursor: pointer; diff --git a/dashboard/src/components/graph/index.tsx b/dashboard/src/components/graph/index.tsx index c98daf82dd..4626fc1335 100644 --- a/dashboard/src/components/graph/index.tsx +++ b/dashboard/src/components/graph/index.tsx @@ -8,7 +8,7 @@ import cls from "classnames" import { css } from "emotion" -import React, { Component, ChangeEvent } from "react" +import React, { Component } from "react" import styled from "@emotion/styled" import { capitalize, uniq } from "lodash" import * as d3 from "d3" @@ -19,13 +19,14 @@ import Card from "../card" import "./graph.scss" import { colors, fontMedium } from "../../styles/variables" import Spinner, { SpinnerProps } from "../spinner" -import CheckBox from "../checkbox" -import { SelectGraphNode } from "../../context/ui" +import { SelectGraphNode, StackGraphSupportedFilterKeys } from "../../context/ui" import { WsEventMessage, SupportedEventName } from "../../context/events" import { Events } from "garden-cli/src/events" import { Extends } from "garden-cli/src/util/util" import { ConfigDump } from "garden-cli/src/garden" import { GraphOutput } from "garden-cli/src/commands/get/get-graph" +import { FiltersButton, Filters } from "../group-filter" +import { RenderedNodeType } from "garden-cli/src/config-graph" interface Node { name: string @@ -111,7 +112,7 @@ function drawChart( g.nodes().forEach(function(v) { const node = g.node(v) // Round the corners of the nodes - node.rx = node.ry = 5 + node.rx = node.ry = 4 // Remove node padding node.paddingBottom = 0 node.paddingTop = 0 @@ -190,11 +191,12 @@ interface Props { graph: GraphOutput onGraphNodeSelected: SelectGraphNode selectedGraphNode: string | null + layoutChanged: boolean message?: WsEventMessage } interface State { - filters: { [key: string]: boolean } + filters: Filters nodes: Node[] edges: Edge[] } @@ -203,7 +205,7 @@ interface State { const makeLabel = (name: string, type: string, moduleName: string) => { return `
-
${type}
+
${capitalize(type)}
${moduleName} ${ @@ -213,17 +215,6 @@ const makeLabel = (name: string, type: string, moduleName: string) => { : `` }
` - // return ` - //
- // - // ${moduleName} - // ${ - // moduleName !== name - // ? ` / - // ${name}` - // : `` - // } - //
` } const Span = styled.span` @@ -239,39 +230,48 @@ const ProcessSpinner = styled(Spinner)` margin: 16px 0 0 20px; ` -// const IconContainer = styled.span` -// display: inline-block; -// width: 3rem; -// height: 3rem; -// background-size: contain; -// background-repeat: no-repeat; -// vertical-align: middle; -// ` +type ChartState = { + nodes: Node[], + edges: Edge[], + filters: Filters, +} class Chart extends Component { _nodes: Node[] _edges: Edge[] _chartRef: React.RefObject - state = { + state: ChartState = { nodes: [], edges: [], - filters: {}, + filters: { + run: { selected: true, label: "Run" }, + deploy: { selected: true, label: "Deploy" }, + test: { selected: true, label: "Test" }, + build: { selected: true, label: "Build" }, + }, } constructor(props) { super(props) this._chartRef = React.createRef() - this.onCheckboxChange = this.onCheckboxChange.bind(this) + this.handleFilter = this.handleFilter.bind(this) this._nodes = [] this._edges = [] - const taskTypes = uniq(this.props.graph.nodes.map(n => n.type)) - const filters = taskTypes.reduce((acc, type) => { - acc[type] = false - return acc - }, {}) + const createFiltersState = + (allGroupFilters, type): Filters => { + return ({ + ...allGroupFilters, + [type]: { + ...(allGroupFilters[type]), + visible: true, + }, + }) + } + const taskTypes: RenderedNodeType[] = uniq(this.props.graph.nodes.map(n => n.type)) + const filters: Filters = taskTypes.reduce(createFiltersState, this.state.filters) this.state = { ...this.state, filters, @@ -279,6 +279,7 @@ class Chart extends Component { } componentDidMount() { + this.drawChart() // Re-draw graph on **end** of window resize event (hence the timer) @@ -295,9 +296,11 @@ class Chart extends Component { window.onresize = () => { } } - onCheckboxChange({ target }: ChangeEvent) { + handleFilter(key: string) { + const toggledFilters = this.state.filters + toggledFilters[key].selected = !toggledFilters[key].selected this.setState({ - filters: { ...this.state.filters, [target.name]: !target.checked }, + filters: toggledFilters, }) } @@ -313,7 +316,7 @@ class Chart extends Component { makeGraph() { const { filters } = this.state const nodes: Node[] = this.props.graph.nodes - .filter(n => !filters[n.type]) + .filter(n => filters[n.type].selected) .map(n => { return { id: n.key, @@ -322,7 +325,7 @@ class Chart extends Component { } }) const edges: Edge[] = this.props.graph.relationships - .filter(n => !filters[n.dependant.type] && !filters[n.dependency.type]) + .filter(n => filters[n.dependant.type].selected && filters[n.dependency.type].selected) .map(r => { const source = r.dependency const target = r.dependant @@ -332,6 +335,7 @@ class Chart extends Component { type: source.type, } }) + return { edges, nodes } } @@ -340,14 +344,14 @@ class Chart extends Component { if (message && message.type === "event") { this.updateNodeClass(message) } - if (prevState.filters !== this.state.filters) { + if (prevState !== this.state) { this.drawChart() } if ( (!prevProps.selectedGraphNode && this.props.selectedGraphNode) || - (prevProps.selectedGraphNode && !this.props.selectedGraphNode) - ) { + (prevProps.selectedGraphNode && !this.props.selectedGraphNode) || + (prevProps.layoutChanged !== this.props.layoutChanged)) { this.drawChart() } if (!this.props.selectedGraphNode) { @@ -382,7 +386,6 @@ class Chart extends Component { render() { const { message } = this.props - const taskTypes = uniq(this.props.graph.nodes.map(n => n.type)) const chartHeightEstimate = `100vh - 2rem` let spinner: React.ReactNode = null @@ -410,20 +413,20 @@ class Chart extends Component { `, )} > - {taskTypes.map(type => { - return ( -
- - {/* {capitalize(type)} */} - {capitalize(type)} - -
- ) - })} +
+ +
+ {status} + {spinner} +
+
{ css` position: absolute; right: 1rem; - bottom: 0; + bottom: 1rem; display: flex; justify-content: flex-end; `, "mr-1", )} > -
- {status} - {spinner} -
{ color: ${colors.gardenPink}; `} > - --{" "} + —{" "} Pending + + + --{" "} + + Processing + + * + * 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 styled from "@emotion/styled" +import { colors } from "../styles/variables" + +interface FilterProps { + selected: boolean +} + +const Filter = styled.li` + padding: .5rem; + border: 1px solid transparent; + box-sizing: border-box; + font-size: 13px; + line-height: 19px; + display: flex; + align-items: center; + text-align: center; + letter-spacing: 0.01em; + color: ${props => (props.selected ? "white" : colors.grayUnselected)}; + background-color: ${props => (props.selected ? colors.gardenGreenDark : "white")}; + box-shadow: 0px 6px 18px rgba(0,0,0,0.06); + margin-right: 0.25rem; + border-radius: 4px; + height: 2rem; + transition: background-color 0.2s ease-in-out; + + &:hover { + cursor: pointer; + background-color: ${props => (!props.selected ? "white" : colors.gardenPink)}; + } +` + +const Filters = styled.ul` + display: flex; +` +const FilterGroup = styled.ul` + display: flex; +` +export type Filters = { + [key in T]: { + label: string + selected: boolean, + readonly?: boolean, + } +} + +interface GroupedFiltersButtonProps { + onFilter: (key: T) => void + groups: Filters[] +} + +export class GroupedFiltersButton extends React.Component> { + + constructor(props) { + super(props) + this.handleFilter = this.handleFilter.bind(this) + } + + handleFilter(event): void { + this.props.onFilter(event.target.id) + } + + render() { + return ( + + {(this.props.groups).map((group, index) => + + {group && Object.keys(group).map((filterKey) => ( + + {group[filterKey].label} + ), + )} + , + )} + + ) + } +} + +interface FiltersButtonProps { + onFilter: (key: T) => void + filters: Filters +} + +export class FiltersButton extends React.Component> { + + constructor(props) { + super(props) + this.handleFilter = this.handleFilter.bind(this) + } + + handleFilter(event): void { + this.props.onFilter(event.target.id) + } + + render() { + const filters = this.props.filters + return ( + + {filters && Object.keys(filters).map((filterKey) => ( + + {filters[filterKey].label} + ), + )} + + ) + } +} diff --git a/dashboard/src/components/info-card.tsx b/dashboard/src/components/info-card.tsx new file mode 100644 index 0000000000..c625958894 --- /dev/null +++ b/dashboard/src/components/info-card.tsx @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * 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, { ReactNode } from "react" +import styled from "@emotion/styled" +import { Entity } from "../containers/overview" +import { colors } from "../styles/variables" +import { Facebook } from "react-content-loader" + +interface InfoCardProps { + type: InfoCardType +} +const InfoCard = styled.div` + max-height: 13rem; + background-color: ${props => (props && props.type && colors.cardTypes[props.type] || "white")}; + margin-right: 1rem; + box-shadow: 0px 0px 16px rgba(0, 0, 0, 0.14); + border-radius: 4px; + width: 100%; + margin-top: 1rem; + + &:first-of-type { + margin-top: 0; + } + + &:last-of-type { + margin-right: 0; + } +` +const Header = styled.div` + width: 100%; + padding: .6rem .75rem; + height: 3rem; +` + +const Content = styled.div` + width: 100%; + padding: 0rem .75rem .75rem .75rem; + position: relative; + max-height: 10rem; + &:empty +{ + display:none; +} +` + +type StateProps = { + state: string, +} +const State = styled.div` + padding: 0 .5rem; + margin-left: auto; + background-color: ${props => (props && props.state && colors.state[props.state] || colors.gardenGrayLight)}; + display: ${props => (props && props.state && colors.state[props.state] && "flex" || "none")}; + align-items: center; + margin-top: -0.5rem; + +border-radius: 4px; + +font-weight: 500; +font-size: 11px; +line-height: 16px; +text-align: center; +letter-spacing: 0.02em; + +color: #FFFFFF; +` + +const Tag = styled.div` + display: flex; + align-items: center; + font-weight: 500; + font-size: 10px; + line-height: 10px; + text-align: right; + letter-spacing: 0.01em; + color: #90A0B7; +` +const Name = styled.div` + height: 1rem; + font-size: 0.9375rem; + color: rgba(0, 0, 0, .87); +` + +const Row = styled.div` + display: flex; + align-items: center; +` +type InfoCardType = "service" | "test" | "task" +interface Props { + type: InfoCardType + children: ReactNode + entity: Entity +} + +export default ({ + children, + type, + entity: { name, isLoading, state }, +}: Props) => { + + return ( + +
+ {type.toUpperCase()} + + {name} + + {state} + + +
+ + {isLoading && ( + + )} + {!isLoading && children} + +
+ ) +} diff --git a/dashboard/src/components/info-pane.tsx b/dashboard/src/components/info-pane.tsx index dfae59a9a2..ae31651f97 100644 --- a/dashboard/src/components/info-pane.tsx +++ b/dashboard/src/components/info-pane.tsx @@ -10,20 +10,22 @@ import React from "react" import cls from "classnames" import { capitalize } from "lodash" import { css } from "emotion" +import moment from "moment" import styled from "@emotion/styled" import Card from "../components/card" import { colors } from "../styles/variables" import { RenderedNode } from "garden-cli/src/config-graph" -import { ErrorNotification } from "./notifications" +import { WarningNotification } from "./notifications" import { ActionIcon } from "./ActionIcon" const Term = styled.div` background-color: ${colors.gardenBlack}; color: white; border-radius: 0.125rem; - max-height: 45rem; + flex: 1 1; overflow-y: auto; padding: 1rem; + margin-top: 1rem; ` const Code = styled.code` font-size: .8rem; @@ -48,11 +50,12 @@ const IconContainer = styled.span` const Key = ({ text }) => (
@@ -63,11 +66,11 @@ const Key = ({ text }) => ( const Value = ({ children }) => (
@@ -75,7 +78,15 @@ const Value = ({ children }) => (
) const Field = ({ children }) => ( -
+
{children}
) @@ -94,8 +105,8 @@ interface Props { onRefresh?: () => void loading?: boolean output?: string | null - startedAt?: string | null - completedAt?: string | null + startedAt?: Date | null + completedAt?: Date | null duration?: string | null } @@ -119,14 +130,29 @@ export const InfoPane: React.FC = ({ {output} ) - } else if (output === null) { - // Output explictly set to null means that the data was fetched but the result was empty - outputEl = No test output + } else if (output === null || output === "") { + // Output explictly set to nullƒ means that the data was fetched but the result was empty + outputEl = ( +
+
+ No {type} output +
+
+ ) } return ( -
+
@@ -174,25 +200,19 @@ export const InfoPane: React.FC = ({ {startedAt && ( - - {startedAt} + + {moment(startedAt).fromNow()} )} {completedAt && ( - - {completedAt} + + {moment(completedAt).fromNow()} )} - {(type === "test" || type === "run") && outputEl !== null && ( - -
- {outputEl} -
-
- )} + {(type === "test" || type === "run") && outputEl !== null && outputEl}
) diff --git a/dashboard/src/components/ingresses.tsx b/dashboard/src/components/ingresses.tsx index 146b5ce206..3c97039018 100644 --- a/dashboard/src/components/ingresses.tsx +++ b/dashboard/src/components/ingresses.tsx @@ -6,19 +6,20 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import React from "react" +import React, { useContext } from "react" import styled from "@emotion/styled" import { ExternalLink } from "./links" import { ServiceIngress } from "garden-cli/src/types/service" import { truncateMiddle } from "../util/helpers" import normalizeUrl from "normalize-url" import { format } from "url" +import { UiStateContext } from "../context/ui" const Ingresses = styled.div` font-size: 1rem; line-height: 1.4rem; color: #4f4f4f; - height: 5rem; + max-height: 5rem; overflow: hidden; overflow-y: auto; @@ -41,11 +42,6 @@ const LinkContainer = styled.div` } ` -const NoIngresses = styled.div` - font-style: italic; - font-size: .75rem; -` - const getIngressUrl = (ingress: ServiceIngress) => { return normalizeUrl(format({ protocol: ingress.protocol, @@ -60,21 +56,39 @@ interface IngressesProp { } export default ({ ingresses }: IngressesProp) => { + const { actions: { selectIngress } } = useContext(UiStateContext) + + const handleSelectIngress = (event) => { + if (ingresses && ingresses.length) { + const ingress = ingresses.find(i => i.path === event.target.id) + if (ingress) { + selectIngress(ingress) + } + } + } + return ( - {ingresses && ingresses.map(i => { - const url = getIngressUrl(i) - return - - {truncateMiddle(url)} - -
-
+ {(ingresses || []).map((ingress, index) => { + const url = getIngressUrl(ingress) + return ( + +
+ + {truncateMiddle(url)} + +
+
+ + {truncateMiddle(url)} + +
+ {ingresses && (index < ingresses.length - 1) && +
+ } +
+ ) })} - {(!ingresses || !ingresses.length) && - - No ingresses found - }
) } diff --git a/dashboard/src/components/links.tsx b/dashboard/src/components/links.tsx index 5593cebc3c..25cef722eb 100644 --- a/dashboard/src/components/links.tsx +++ b/dashboard/src/components/links.tsx @@ -13,15 +13,15 @@ import { NavLink as ReactRouterNavLink } from "react-router-dom" import { colors } from "../styles/variables" export const ExternalLink = styled.a` + cursor: pointer; text-decoration: underline; &:visited { - color: ${colors.gardenGreenDark} } &:hover { - color: ${colors.gardenPink} + color: ${colors.gardenPink}; } ` export const NavLink = props => ( - + ) diff --git a/dashboard/src/components/logs.tsx b/dashboard/src/components/logs.tsx index a59e4babf8..df2590bb73 100644 --- a/dashboard/src/components/logs.tsx +++ b/dashboard/src/components/logs.tsx @@ -101,7 +101,7 @@ class Logs extends Component { const filteredLogs = value === "all" ? logs : logs.filter(l => l.serviceName === value) return ( -
+
` + padding-top: .75rem; display: flex; flex-wrap: wrap; align-items: middle; + display: ${props => (props.visible ? `block` : "none")}; + animation: fadein .5s ; + + @keyframes fadein { + from { + opacity: 0; + } + to { + opacity: 1; + } + } ` + const Header = styled.div` display: flex; align-items: center; + align-self: flex-start; ` -const Label = styled.div` - font-size: .75rem; +type FieldsProps = { + visible: boolean, +} +const Fields = styled.div` + display: ${props => (props.visible ? `block` : "none")}; + + animation: fadein .5s ; +&:first-of-type{ + padding-top:0; +} + @keyframes fadein { + from { + opacity: 0; + } + to { + opacity: 1; + } + } +` + +const Field = styled.div` + padding-bottom: .5rem; + max-width: 14rem; + + &:last-of-type{ + padding-bottom: 0; + } +` + +const Tag = styled.div` display: flex; align-items: center; - color: #bcbcbc; + font-weight: 500; + font-size: 10px; + letter-spacing: 0.01em; + color: #90A0B7; ` const Name = styled.div` padding-right: .5rem; + font-weight: 500; + font-size: 15px; + letter-spacing: 0.01em; + color: #323C47; ` +const Key = styled.div` + padding-right: .5rem; + font-size: 13px; + line-height: 19px; + letter-spacing: 0.01em; + color: #4C5862; + opacity: 0.5; +` +const Value = styled.div` + padding-right: .5rem; + font-size: 13px; + line-height: 19px; + letter-spacing: 0.01em; + color: #4C5862; +` + +const UrlFull = styled(Value)` + overflow-wrap: break-word; + word-wrap: break-word; + -ms-word-break: break-all; + word-break: break-all; + word-break: break-word; + -ms-hyphens: auto; + -moz-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; + cursor: pointer; +` +const Description = styled(Field)` + padding-top: 0.25rem; +` + +const UrlShort = styled(Value)` + padding-right: .5rem; + font-size: 13px; + line-height: 19px; + letter-spacing: 0.01em; + color: #4C5862; + cursor: pointer; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +` interface ModuleProp { module: ModuleModel } export default ({ - module: { services = [], name }, + module: { services = [], tests = [], tasks = [], name, type, description }, }: ModuleProp) => { + const { + state: { overview: { filters } }, + } = useContext(UiStateContext) + + const [showFullDescription, setDescriptionState] = useState(false) + const toggleDescriptionState = () => (setDescriptionState(!showFullDescription)) return ( - +
{name} - - + {type && type.toUpperCase()} MODULE
- + + {description && ( + + {!showFullDescription && ( + {description} + )} + {showFullDescription && ( + {description} + )} + + )} + + {services.map(service => ( - + + + + + + + ))} - + + + {tests.map(test => ( + + {filters.testsInfo && +
+
+ Last run + {moment(test.startedAt).fromNow()} +
+ {test.state === "succeeded" && +
+ Duration + {test.duration} +
+ } +
+ } +
+ ))} +
+ + {tasks.map(task => ( + + {filters.tasksInfo && +
+ {task.startedAt && ( +
+ Last run + {moment(task.startedAt).fromNow()} +
+ )} + {task.state === "succeeded" && +
+ Duration + {task.duration} +
+ } +
+ } +
+ ))} +
+
) } diff --git a/dashboard/src/components/notifications.tsx b/dashboard/src/components/notifications.tsx index 0184a37650..c098f8c0fb 100644 --- a/dashboard/src/components/notifications.tsx +++ b/dashboard/src/components/notifications.tsx @@ -13,7 +13,6 @@ import { colors } from "../styles/variables" const Notification = styled.div` border-radius: 3px 3px 3px 3px; padding: .5rem; - margin-top: .5rem; font-size: 0.75rem; display: flex; align-items: center; diff --git a/dashboard/src/components/service.tsx b/dashboard/src/components/service.tsx deleted file mode 100644 index e09362f55e..0000000000 --- a/dashboard/src/components/service.tsx +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) 2018 Garden Technologies, Inc. - * - * 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 { css } from "emotion" -import styled from "@emotion/styled" -import { ReactComponent as VIcon } from "./../assets/v.svg" -import Ingresses from "./ingresses" -import { ServiceModel } from "../containers/overview" -import { ServiceState } from "garden-cli/src/types/service" -import { colors } from "../styles/variables" -import { Facebook } from "react-content-loader" - -const Service = styled.div` - width: 17rem; - height: 13rem; - background-color: white; - margin-bottom: 1rem; - box-shadow: 0px 1px 5px rgba(0, 0, 0, .2), - 0px 3px 4px rgba(255, 255, 255, .12), 0px 2px 4px rgb(255, 255, 255); - margin-right: 1rem; - &:last-of-type { - margin-right: 0; - } -` -const Header = styled.div` - width: 100%; - padding: .4rem .75rem; - border-bottom: 1px solid #c4c4c4; - height: 3rem; -` - -const Fields = styled.div` - animation: fadein .75s; - - @keyframes fadein { - from { - opacity: 0; - } - to { - opacity: 1; - } - } -` -const Field = styled.div` - padding-bottom: .5rem; -` -const Label = styled.div` - font-size: .75rem; - line-height: 1rem; - color: #878787; -` -const Value = styled.div` - font-size: 1rem; - line-height: 1.4rem; - color: #4f4f4f; -` - -const Content = styled.div` - width: 100%; - padding: .75rem .75rem .75rem .75rem; - position: relative; - height: 10rem; -` - -type StateProps = { - state?: ServiceState, -} -const State = styled.div` - width: 1.5rem; - height: 1.5rem; - border-radius: 2rem; - margin-left: auto; - background-color: ${props => (props && props.state && colors.status[props.state] || colors.gardenGrayLight)}; - display: flex; - align-items: center; -` - -const Tag = styled.div` - font-size: .56rem; - display: flex; - align-items: center; - color: #bcbcbc; -` -const Name = styled.div` - height: 1.5rem; - font-size: 1.25rem; - color: rgba(0, 0, 0, .87); -` - -const Row = styled.div` - display: flex; - align-items: center; -` - -interface ServiceProp { - service: ServiceModel -} -export default ({ - service: { name, ingresses, state, isLoading }, -}: ServiceProp) => { - - return ( - -
- SERVICE - - {name} - - - - -
- - {isLoading && ( - - )} - {!isLoading && ( - - - - {state} - - - - - - - )} - - -
- ) -} diff --git a/dashboard/src/components/sidebar.tsx b/dashboard/src/components/sidebar.tsx index 1ff49d4e5a..ce5aa44011 100644 --- a/dashboard/src/components/sidebar.tsx +++ b/dashboard/src/components/sidebar.tsx @@ -13,7 +13,7 @@ import React, { Component } from "react" import { NavLink } from "./links" import { Page } from "../containers/sidebar" -import { colors, fontMedium } from "../styles/variables" +import { colors, fontRegular } from "../styles/variables" interface Props { pages: Page[] @@ -24,7 +24,7 @@ interface State { } const Button = styled.li` - ${fontMedium}; + ${fontRegular}; border-radius: 2px; cursor: pointer; width: 100%; diff --git a/dashboard/src/components/terminal.tsx b/dashboard/src/components/terminal.tsx index 97c6717b89..94e98858b8 100644 --- a/dashboard/src/components/terminal.tsx +++ b/dashboard/src/components/terminal.tsx @@ -22,8 +22,7 @@ interface Props { const Term = styled.div` background-color: ${colors.gardenBlack}; border-radius: 2px; - max-height: 45rem; - max-width: calc(18wv - 4rem); + max-height: calc(100vh - 9rem); overflow-y: auto; ` diff --git a/dashboard/src/components/view-ingress.tsx b/dashboard/src/components/view-ingress.tsx new file mode 100644 index 0000000000..c6f9eadf91 --- /dev/null +++ b/dashboard/src/components/view-ingress.tsx @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * 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, { useContext } from "react" +import styled from "@emotion/styled" +import { ExternalLink } from "./links" +import { ServiceIngress } from "garden-cli/src/types/service" +import { truncateMiddle } from "../util/helpers" +import normalizeUrl from "normalize-url" +import { format } from "url" +import { UiStateContext } from "../context/ui" +import { ActionIcon } from "./ActionIcon" + +const ViewIngress = styled.div` + ` + +const LinkContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + position: absolute; + top: 1.5rem; + right: 1rem; + background: white; + padding: 0.5rem; + border-radius: 4px; + border-bottom-right-radius: 0; + border-top-left-radius: 0; + box-shadow: 0px 6px 18px rgba(0,0,0,0.06); + +` + +type FrameWrapperProps = { + width?: string, + height?: string, +} + +const FrameWrapper = styled.div` + display: flex; + flex-direction: column; + width: ${props => (props.width || "50vw")}; + height: ${props => (props.height || "96.5vh")}; + background: white; + box-shadow: 0px 6px 18px rgba(0,0,0,0.06); + border-radius: 4px; + min-height: 0; + overflow: hidden; +` + +const Frame = styled.iframe` + flex: 1 1 auto; + border: 0; + width: 100%; +` + +const getIngressUrl = (ingress: ServiceIngress) => { + return normalizeUrl(format({ + protocol: ingress.protocol, + hostname: ingress.hostname, + port: ingress.port, + pathname: ingress.path, + })) +} + +interface ViewIngressProp { + ingress: ServiceIngress, + height?: string, + width?: string, +} + +export default ({ ingress, height, width }: ViewIngressProp) => { + const { actions: { selectIngress } } = useContext(UiStateContext) + + const removeSelectedIngress = () => { + selectIngress(null) + } + + const url = getIngressUrl(ingress) + + return ( + + + + {truncateMiddle(url)} + + + + + + + + + + ) +} diff --git a/dashboard/src/containers/graph.tsx b/dashboard/src/containers/graph.tsx index 64cfc27b55..394002a653 100644 --- a/dashboard/src/containers/graph.tsx +++ b/dashboard/src/containers/graph.tsx @@ -7,7 +7,7 @@ */ import React, { useContext, useEffect } from "react" - +import styled from "@emotion/styled" import Graph from "../components/graph" import PageError from "../components/page-error" import { EventContext } from "../context/events" @@ -16,6 +16,10 @@ import { UiStateContext } from "../context/ui" import { NodeInfo } from "./node-info" import Spinner from "../components/spinner" +const Wrapper = styled.div` +padding-left: .75rem; +` + export default () => { const { actions: { loadGraph, loadConfig }, @@ -28,7 +32,7 @@ export default () => { const { actions: { selectGraphNode }, - state: { selectedGraphNode }, + state: { selectedGraphNode, isSidebarOpen }, } = useContext(UiStateContext) if (config.error || graph.error) { @@ -44,7 +48,7 @@ export default () => { const node = graph.data.nodes.find(n => n.key === selectedGraphNode) if (node) { moreInfoPane = ( -
+
) @@ -52,17 +56,18 @@ export default () => { } return ( -
-
+ +
{moreInfoPane} -
+ ) } diff --git a/dashboard/src/containers/node-info.tsx b/dashboard/src/containers/node-info.tsx index c712a12b05..f69ef47400 100644 --- a/dashboard/src/containers/node-info.tsx +++ b/dashboard/src/containers/node-info.tsx @@ -8,7 +8,7 @@ import React, { useContext, useEffect } from "react" import { DataContext } from "../context/data" -import { timeConversion } from "../util/helpers" +import { getDuration } from "../util/helpers" import { UiStateContext } from "../context/ui" import { RenderedNode } from "garden-cli/src/config-graph" import { InfoPane } from "../components/info-pane" @@ -27,23 +27,14 @@ export interface Props { } function prepareData(data: TestResultOutput | TaskResultOutput) { + const startedAt = data.startedAt + const completedAt = data.completedAt const duration = - data.startedAt && - data.completedAt && - timeConversion( - new Date(data.completedAt).valueOf() - - new Date(data.startedAt).valueOf(), - ) - const startedAt = - data.startedAt && - new Date(data.startedAt).toLocaleString() - - const completedAt = - data.completedAt && - new Date(data.completedAt).toLocaleString() + startedAt && + completedAt && + getDuration(startedAt, completedAt) const output = data.output - return { duration, startedAt, completedAt, output } } diff --git a/dashboard/src/containers/overview.tsx b/dashboard/src/containers/overview.tsx index a6b07ba527..82e739da8e 100644 --- a/dashboard/src/containers/overview.tsx +++ b/dashboard/src/containers/overview.tsx @@ -10,10 +10,14 @@ import React, { useContext, useEffect } from "react" import PageError from "../components/page-error" import styled from "@emotion/styled" import { ServiceIngress } from "garden-cli/src/types/service" +import { RunState } from "garden-cli/src/commands/get/get-status" import Module from "../components/module" +import { default as ViewIngress } from "../components/view-ingress" import { DataContext } from "../context/data" import Spinner from "../components/spinner" import { ServiceState } from "garden-cli/src/types/service" +import { UiStateContext } from "../context/ui" +import { getDuration } from "../util/helpers" export const overviewConfig = { service: { @@ -21,21 +25,48 @@ export const overviewConfig = { }, } +const Overview = styled.div` + padding-top: .5rem; +` + const Modules = styled.div` - padding-top: 1rem; + margin-top: 1rem; display: flex; flex-wrap: wrap; + overflow-y: scroll; + max-height: calc(100vh - 2rem); + padding: 0 0 0 1rem; ` export type ModuleModel = { name: string; - services: ServiceModel[]; + type: string; + path: string; + description?: string; + services: Service[]; + tests: Test[]; + tasks: Task[]; } -export type ServiceModel = { - ingresses?: ServiceIngress[]; +export type Entity = { name: string; - state?: ServiceState; isLoading: boolean; + state: ServiceState | RunState; +} +export interface Service extends Entity { + state: ServiceState + ingresses?: ServiceIngress[] +} +export interface Test extends Entity { + startedAt?: Date + completedAt?: Date + duration?: string + state: RunState +} +export interface Task extends Entity { + startedAt?: Date + completedAt?: Date + duration?: string + state: RunState } // Note: We render the overview page components individually so we that we don't @@ -46,6 +77,9 @@ export default () => { store: { config, status }, } = useContext(DataContext) + const { + state: { overview: { selectedIngress } } } = useContext(UiStateContext) + useEffect(loadConfig, []) useEffect(loadStatus, []) @@ -63,15 +97,28 @@ export default () => { // fill modules with services names modules = config.data.moduleConfigs.map(moduleConfig => ({ name: moduleConfig.name, + type: moduleConfig.type, + path: moduleConfig.path, + description: moduleConfig.description, services: moduleConfig.serviceConfigs.map(service => ({ name: service.name, isLoading: true, - })), + })) as Service[], + tests: moduleConfig.testConfigs.map(test => ({ + name: test.name, + isLoading: true, + })) as Test[], + tasks: moduleConfig.taskConfigs.map(task => ({ + name: task.name, + isLoading: true, + })) as Task[], })) // fill missing data from status if (status.data && status.data.services) { const servicesStatus = status.data.services + const testsStatus = status.data.tests + const tasksStatus = status.data.tasks for (let currModule of modules) { for (let serviceName of Object.keys(servicesStatus)) { const index = currModule.services.findIndex(s => s.name === serviceName) @@ -79,21 +126,71 @@ export default () => { if (index !== -1) { currModule.services[index] = { ...currModule.services[index], - state: servicesStatus[serviceName].state, + state: servicesStatus[serviceName].state || "unknown", ingresses: servicesStatus[serviceName].ingresses, isLoading: false, - } as ServiceModel + } + } + } + + for (let testName of Object.keys(testsStatus)) { + const index = currModule.tests.findIndex(t => t.name === testName.split(".")[1]) + + if (index !== -1) { + const testStatus = testsStatus[testName] + currModule.tests[index] = { + ...currModule.tests[index], + state: testStatus.state || "outdated", + isLoading: false, + startedAt: testStatus.startedAt, + completedAt: testStatus.completedAt, + duration: testStatus.startedAt && + testStatus.completedAt && + getDuration(testStatus.startedAt, testStatus.completedAt), + } + } + } + + for (let taskName of Object.keys(tasksStatus)) { + const index = currModule.tasks.findIndex(t => t.name === taskName) + + if (index !== -1) { + const taskStatus = tasksStatus[taskName] + currModule.tasks[index] = { + ...currModule.tasks[index], + state: taskStatus.state || "outdated", + isLoading: false, + startedAt: taskStatus.startedAt, + completedAt: taskStatus.completedAt, + duration: taskStatus.startedAt && + taskStatus.completedAt && + getDuration(taskStatus.startedAt, taskStatus.completedAt), + } } } } } modulesContainerComponent = ( - - {modules.map(module => ( - - ))} - + +
+
+ + {modules.map(module => ( + + ))} + +
+ {selectedIngress && +
+ {selectedIngress && + + } +
+ } + +
+
) } diff --git a/dashboard/src/context/data.tsx b/dashboard/src/context/data.tsx index 896c97a422..90831184fc 100644 --- a/dashboard/src/context/data.tsx +++ b/dashboard/src/context/data.tsx @@ -24,7 +24,7 @@ import { ServiceLogEntry } from "garden-cli/src/types/plugin/outputs" import { ConfigDump } from "garden-cli/src/garden" import { GraphOutput } from "garden-cli/src/commands/get/get-graph" import { TaskResultOutput } from "garden-cli/src/commands/get/get-task-result" -import { EnvironmentStatus } from "garden-cli/src/actions" +import { StatusCommandResult } from "garden-cli/src/commands/get/get-status" import { TestResultOutput } from "garden-cli/src/commands/get/get-test-result" import { AxiosError } from "axios" @@ -39,7 +39,7 @@ interface Store { data?: ConfigDump, }, status: StoreCommon & { - data?: EnvironmentStatus, + data?: StatusCommandResult, }, graph: StoreCommon & { data?: GraphOutput, diff --git a/dashboard/src/context/ui.tsx b/dashboard/src/context/ui.tsx index f02b85ea91..efd0a4bbb2 100644 --- a/dashboard/src/context/ui.tsx +++ b/dashboard/src/context/ui.tsx @@ -7,21 +7,62 @@ */ import React, { useState } from "react" +import { ServiceIngress } from "garden-cli/src/types/service" +import { RenderedNodeType } from "garden-cli/src/config-graph" interface UiState { isSidebarOpen: boolean - selectedGraphNode: string | null + overview: { + selectedIngress: ServiceIngress | null, + filters: { + [key in OverviewSupportedFilterKeys]: boolean + }, + }, + stackGraph: { + filters: { + [key in StackGraphSupportedFilterKeys]: boolean + }, + }, + selectedGraphNode: string | null, } export type SelectGraphNode = (node: string) => void +export type SelectIngress = (ingress: ServiceIngress | null) => void + +export type OverviewSupportedFilterKeys = "modules" | "modulesInfo" | "services" | "servicesInfo" | + "tasks" | "tasksInfo" | "tests" | "testsInfo" +export type StackGraphSupportedFilterKeys = Exclude interface UiActions { toggleSidebar: () => void + overviewToggleItemsView: (itemToToggle: OverviewSupportedFilterKeys) => void selectGraphNode: SelectGraphNode + selectIngress: SelectIngress clearGraphNodeSelection: () => void } const INITIAL_UI_STATE: UiState = { + overview: { + selectedIngress: null, + filters: { + modules: true, + modulesInfo: true, + services: true, + servicesInfo: true, + tasks: true, + tasksInfo: true, + tests: true, + testsInfo: true, + }, + }, + stackGraph: { // todo: currently not attached to graph/index.tsx, use context there + filters: { + build: true, + run: true, + deploy: true, + test: true, + }, + }, isSidebarOpen: true, selectedGraphNode: null, } @@ -43,13 +84,34 @@ const useUiState = () => { }) } + const overviewToggleItemsView = (itemToToggle: OverviewSupportedFilterKeys) => { + setState({ + ...uiState, + overview: { + ...uiState.overview, + filters: { + ...uiState.overview.filters, + [itemToToggle]: !uiState.overview.filters[itemToToggle], + }, + }, + }) + } + const selectGraphNode = (node: string) => { setState({ ...uiState, selectedGraphNode: node, }) } - + const selectIngress = (ingress: ServiceIngress | null) => { + setState({ + ...uiState, + overview: { + ...uiState.overview, + selectedIngress: ingress, + }, + }) + } const clearGraphNodeSelection = () => { setState({ ...uiState, @@ -61,8 +123,10 @@ const useUiState = () => { state: uiState, actions: { toggleSidebar, + overviewToggleItemsView, selectGraphNode, clearGraphNodeSelection, + selectIngress, }, } } diff --git a/dashboard/src/styles/variables.ts b/dashboard/src/styles/variables.ts index ad93329173..20aa58063f 100644 --- a/dashboard/src/styles/variables.ts +++ b/dashboard/src/styles/variables.ts @@ -27,6 +27,7 @@ export const colors = { gray: "#f0f0f0", black: "#192A3E", grayLight: "#fafafa", + grayUnselected: "#C2CFE0", gardenGray: "#555656", gardenGrayLight: "#cdcfd1", gardenGrayLighter: "#FBFCFD", @@ -60,10 +61,19 @@ export const colors = { backgroundColor: "#BEF", }, }, - status: { + state: { ready: "#2ED47A", + succeeded: "#2ED47A", + failed: "#F7685B", deploying: "#FFB946", + stopped: "#FFB946", + unknown: "#FFB946", missing: "#F7685B", unhealthy: "#F7685B", }, + cardTypes: { + service: "", + test: "", + run: "", + }, } diff --git a/dashboard/src/util/helpers.ts b/dashboard/src/util/helpers.ts index 969868c0d2..b6acd027f5 100644 --- a/dashboard/src/util/helpers.ts +++ b/dashboard/src/util/helpers.ts @@ -33,9 +33,18 @@ export function timeConversion(millisec) { return timeFormatted } -export const truncateMiddle = (str) => { - if (str.length > 35) { - return str.substr(0, 16) + "..." + str.substr(str.length - 16, str.length) +// function expects either a string in the form of "2019-05-18T08:30:08.601Z" or a Date +export function getDuration(start: string | Date, end: string | Date): string { + const startValue = new Date(start).valueOf() + const endValue = new Date(end).valueOf() + const duration = timeConversion(endValue - startValue) + return duration +} + +export const truncateMiddle = (str: string, resLength: number = 35) => { + if (str.length > resLength) { + const middle = Math.ceil(resLength / 2) + return str.substr(0, middle) + "..." + str.substr(str.length - middle, str.length) } return str diff --git a/garden-service/package-lock.json b/garden-service/package-lock.json index 54992f50f3..ef44469f6e 100644 --- a/garden-service/package-lock.json +++ b/garden-service/package-lock.json @@ -3688,7 +3688,7 @@ }, "readable-stream": { "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "requires": { "core-util-is": "~1.0.0", @@ -12166,7 +12166,7 @@ "dependencies": { "bluebird": { "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "resolved": "http://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=" }, "duplexer2": { diff --git a/garden-service/src/commands/get/get-status.ts b/garden-service/src/commands/get/get-status.ts index 6d9c1cc1b7..b3f574812b 100644 --- a/garden-service/src/commands/get/get-status.ts +++ b/garden-service/src/commands/get/get-status.ts @@ -20,16 +20,23 @@ import { ConfigGraph } from "../../config-graph" import { getTaskVersion } from "../../tasks/task" import { LogEntry } from "../../logger/log-entry" import { getTestVersion } from "../../tasks/test" +import { RunResult } from "../../types/plugin/outputs" -type RunStatus = "not-completed" | "completed" +export type RunState = "outdated" | "succeeded" | "failed" -interface TestStatuses { [testKey: string]: RunStatus } -interface TaskStatuses { [taskKey: string]: RunStatus } +export interface RunStatus { + state: RunState + startedAt?: Date + completedAt?: Date +} + +export interface TestStatuses { [testKey: string]: RunStatus } +export interface TaskStatuses { [taskKey: string]: RunStatus } // Value is "completed" if the test/task has been run for the current version. export interface StatusCommandResult extends EnvironmentStatus { - testStatuses: TestStatuses - taskStatuses: TaskStatuses + tests: TestStatuses + tasks: TaskStatuses } export class GetStatusCommand extends Command { @@ -44,8 +51,8 @@ export class GetStatusCommand extends Command { const graph = await garden.getConfigGraph() result = await Bluebird.props({ ...status, - testStatuses: getTestStatuses(garden, graph, log), - taskStatuses: getTaskStatuses(garden, graph, log), + tests: getTestStatuses(garden, graph, log), + tasks: getTaskStatuses(garden, graph, log), }) } else { result = status @@ -66,10 +73,10 @@ async function getTestStatuses(garden: Garden, configGraph: ConfigGraph, log: Lo return fromPairs(flatten(await Bluebird.map(modules, async (module) => { return Bluebird.map(module.testConfigs, async (testConfig) => { const testVersion = await getTestVersion(garden, configGraph, module, testConfig) - const done = !!(await garden.actions.getTestResult({ + const result = await garden.actions.getTestResult({ module, log, testVersion, testName: testConfig.name, - })) - return [`${module.name}.${testConfig.name}`, done ? "completed" : "not-completed"] + }) + return [`${module.name}.${testConfig.name}`, runStatus(result)] }) }))) } @@ -78,7 +85,16 @@ async function getTaskStatuses(garden: Garden, configGraph: ConfigGraph, log: Lo const tasks = await configGraph.getTasks() return fromPairs(await Bluebird.map(tasks, async (task) => { const taskVersion = await getTaskVersion(garden, configGraph, task) - const done = !!(await garden.actions.getTaskResult({ task, taskVersion, log })) - return [task.name, done ? "completed" : "not-completed"] + const result = await garden.actions.getTaskResult({ task, taskVersion, log }) + return [task.name, runStatus(result)] })) } + +function runStatus(result: R | null): RunStatus { + if (result) { + const { startedAt, completedAt } = result + return { startedAt, completedAt, state: result.success ? "succeeded" : "failed" } + } else { + return { state: "outdated" } + } +} diff --git a/garden-service/src/server/commands.ts b/garden-service/src/server/commands.ts index 5d2e597a7f..dbcf612148 100644 --- a/garden-service/src/server/commands.ts +++ b/garden-service/src/server/commands.ts @@ -96,7 +96,7 @@ export async function prepareCommands(): Promise { parameters: Joi.object() .keys({ ...paramsToJoi(command.arguments), - ...paramsToJoi(command.options), + ...paramsToJoi({ ...GLOBAL_OPTIONS, ...command.options }), }) .unknown(false), }) diff --git a/package-lock.json b/package-lock.json index de8788edac..5eec0f93af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4255,7 +4255,8 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4279,13 +4280,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4302,19 +4305,22 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4438,13 +4444,15 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "dev": true, + "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4461,6 +4469,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4469,13 +4478,15 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4496,6 +4507,7 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4591,7 +4603,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4605,6 +4618,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4678,7 +4692,8 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4720,6 +4735,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4741,6 +4757,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4782,13 +4799,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "dev": true + "dev": true, + "optional": true } } },