diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index c14a4b12f2..e392103c56 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -977,6 +977,57 @@ "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==", "dev": true }, + "@emotion/babel-utils": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/@emotion/babel-utils/-/babel-utils-0.6.10.tgz", + "integrity": "sha512-/fnkM/LTEp3jKe++T0KyTszVGWNKPNOUJfjNKLO17BzQ6QPxgbg3whayom1Qr2oLFH3V92tDymU+dT5q676uow==", + "requires": { + "@emotion/hash": "^0.6.6", + "@emotion/memoize": "^0.6.6", + "@emotion/serialize": "^0.9.1", + "convert-source-map": "^1.5.1", + "find-root": "^1.1.0", + "source-map": "^0.7.2" + }, + "dependencies": { + "@emotion/hash": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.6.6.tgz", + "integrity": "sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ==" + }, + "@emotion/memoize": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.6.6.tgz", + "integrity": "sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ==" + }, + "@emotion/serialize": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.9.1.tgz", + "integrity": "sha512-zTuAFtyPvCctHBEL8KZ5lJuwBanGSutFEncqLn/m9T1a6a93smBStK+bZzcNPgj4QS8Rkw9VTwJGhRIUVO8zsQ==", + "requires": { + "@emotion/hash": "^0.6.6", + "@emotion/memoize": "^0.6.6", + "@emotion/unitless": "^0.6.7", + "@emotion/utils": "^0.8.2" + } + }, + "@emotion/unitless": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.6.7.tgz", + "integrity": "sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg==" + }, + "@emotion/utils": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.8.2.tgz", + "integrity": "sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw==" + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } + } + }, "@emotion/cache": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.0.tgz", @@ -1573,6 +1624,26 @@ "@types/react-router": "*" } }, + "@types/react-select": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-2.0.11.tgz", + "integrity": "sha512-kITn4R50eUJCi2YT3JFZS4z5M2SJJqqYiVUX1HyLSFWbHbF6J25ZPKCCXANQrsnQzSrac2XiNpR5oYBif6l93g==", + "dev": true, + "requires": { + "@types/react": "*", + "@types/react-dom": "*", + "@types/react-transition-group": "*" + } + }, + "@types/react-transition-group": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-2.0.15.tgz", + "integrity": "sha512-S0QnNzbHoWXDbKBl/xk5dxA4FT+BNlBcI3hku991cl8Cz3ytOkUMcCRtzdX11eb86E131bSsQqy5WrPCdJYblw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/tapable": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.2.tgz", @@ -5538,6 +5609,29 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "requires": { + "@babel/runtime": "^7.1.2" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.1.tgz", + "integrity": "sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==", + "requires": { + "regenerator-runtime": "^0.12.0" + } + }, + "regenerator-runtime": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" + } + } + }, "dom-serializer": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", @@ -11570,6 +11664,11 @@ "mimic-fn": "^1.0.0" } }, + "memoize-one": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-4.0.3.tgz", + "integrity": "sha512-QmpUu4KqDmX0plH4u+tf0riMc1KHE1+lw95cMrLlXQAFOx/xnBtwhZ52XJxd9X2O6kwKBqX32kmhbhlobD0cuw==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -17290,7 +17389,6 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz", "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==", - "dev": true, "requires": { "performance-now": "^2.1.0" } @@ -17616,6 +17714,19 @@ "integrity": "sha512-7kEBKwU9R8fKnZJBRa5RSIfay4KJwnYvKB6gODGicUmDSAhQJ7Tdnll5S0RLtYrzRfMVXlqYw61rzrSpP4ThLQ==", "dev": true }, + "react-input-autosize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz", + "integrity": "sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA==", + "requires": { + "prop-types": "^15.5.8" + } + }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "react-router": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz", @@ -17750,6 +17861,95 @@ } } }, + "react-select": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-2.3.0.tgz", + "integrity": "sha512-CD3jyZs5lwy/CHW3SdYU1d1FtmJxgvVPdKMwEE8dD6MyNANFtvW95P/V20StPsPppFY7oePv/i2Mun2C2+WgTA==", + "requires": { + "classnames": "^2.2.5", + "emotion": "^9.1.2", + "memoize-one": "^4.0.0", + "prop-types": "^15.6.0", + "raf": "^3.4.0", + "react-input-autosize": "^2.2.1", + "react-transition-group": "^2.2.1" + }, + "dependencies": { + "@emotion/hash": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.6.6.tgz", + "integrity": "sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ==" + }, + "@emotion/memoize": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.6.6.tgz", + "integrity": "sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ==" + }, + "@emotion/stylis": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.7.1.tgz", + "integrity": "sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ==" + }, + "@emotion/unitless": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.6.7.tgz", + "integrity": "sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg==" + }, + "babel-plugin-emotion": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz", + "integrity": "sha512-dgCImifnOPPSeXod2znAmgc64NhaaOjGEHROR/M+lmStb3841yK1sgaDYAYMnlvWNz8GnpwIPN0VmNpbWYZ+VQ==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@emotion/babel-utils": "^0.6.4", + "@emotion/hash": "^0.6.2", + "@emotion/memoize": "^0.6.1", + "@emotion/stylis": "^0.7.0", + "babel-plugin-macros": "^2.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "convert-source-map": "^1.5.0", + "find-root": "^1.1.0", + "mkdirp": "^0.5.1", + "source-map": "^0.5.7", + "touch": "^2.0.1" + } + }, + "create-emotion": { + "version": "9.2.12", + "resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-9.2.12.tgz", + "integrity": "sha512-P57uOF9NL2y98Xrbl2OuiDQUZ30GVmASsv5fbsjF4Hlraip2kyAvMm+2PoYUvFFw03Fhgtxk3RqZSm2/qHL9hA==", + "requires": { + "@emotion/hash": "^0.6.2", + "@emotion/memoize": "^0.6.1", + "@emotion/stylis": "^0.7.0", + "@emotion/unitless": "^0.6.2", + "csstype": "^2.5.2", + "stylis": "^3.5.0", + "stylis-rule-sheet": "^0.0.10" + } + }, + "emotion": { + "version": "9.2.12", + "resolved": "https://registry.npmjs.org/emotion/-/emotion-9.2.12.tgz", + "integrity": "sha512-hcx7jppaI8VoXxIWEhxpDW7I+B4kq9RNzQLmsrF6LY8BGKqe2N+gFAQr0EfuFucFlPs2A9HM4+xNj4NeqEWIOQ==", + "requires": { + "babel-plugin-emotion": "^9.2.11", + "create-emotion": "^9.2.12" + } + } + } + }, + "react-transition-group": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.3.tgz", + "integrity": "sha512-2DGFck6h99kLNr8pOFk+z4Soq3iISydwOFeeEVPjTN6+Y01CmvbWmnN02VuTWyFdnRtIDPe+wy2q6Ui8snBPZg==", + "requires": { + "dom-helpers": "^3.3.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -19304,6 +19504,16 @@ } } }, + "stylis": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", + "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==" + }, + "stylis-rule-sheet": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", + "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==" + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -19828,6 +20038,24 @@ "hoek": "4.x.x" } }, + "touch": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/touch/-/touch-2.0.2.tgz", + "integrity": "sha512-qjNtvsFXTRq7IuMLweVgFxmEuQ6gLbRs2jQxL80TtZ31dEKWYIxRXquij6w6VimyDek5hD3PytljHmEtAs2u0A==", + "requires": { + "nopt": "~1.0.10" + }, + "dependencies": { + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "requires": { + "abbrev": "1" + } + } + } + }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index 91466a4360..b1b5b25e9a 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -3,7 +3,6 @@ "version": "0.1.0", "private": true, "devDependencies": { - "fs-extra": "^7.0.1", "@types/d3": "^5.0.1", "@types/dagre-d3": "^0.4.38", "@types/fs-extra": "^5.0.4", @@ -14,6 +13,8 @@ "@types/react": "^16.7.18", "@types/react-dom": "^16.0.11", "@types/react-router-dom": "^4.3.1", + "@types/react-select": "^2.0.11", + "fs-extra": "^7.0.1", "gulp": "^4.0.0", "gulp-tslint": "^8.1.3", "gulp-typescript": "^5.0.0", @@ -37,6 +38,7 @@ "react": "16.7.0-alpha.2", "react-dom": "16.7.0-alpha.2", "react-router-dom": "^4.3.1", + "react-select": "^2.3.0", "uuid": "^3.3.2" }, "scripts": { diff --git a/dashboard/src/components/checkbox.tsx b/dashboard/src/components/checkbox.tsx index d13e18cacd..aa2e104118 100644 --- a/dashboard/src/components/checkbox.tsx +++ b/dashboard/src/components/checkbox.tsx @@ -6,19 +6,81 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import styled from "@emotion/styled/macro" import React, { ChangeEvent } from "react" +import { colors } from "../styles/variables" + +const Label = styled.label` + display: block; + position: relative; + padding-left: 35px; + margin-bottom: 12px; + cursor: pointer; + font-size: 1.1rem; + user-select: none; + :hover { + input, span { + background-color: ${colors.gray}; + } + } +` + +const Input = styled.input` + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; + :checked { + background-color: ${colors.gardenPink} !important; + } +` + +const Checkmark = styled.span` + position: absolute; + top: 0; + left: 0; + height: 21px; + width: 21px; + border: 1px solid ${colors.darkGray}; + :after { + content: ""; + position: absolute; + display: none; + left: 7px; + top: 3px; + width: 7px; + height: 12px; + border: solid white; + border-width: 0 3px 3px 0; + transform: rotate(45deg); + } +` + +const CheckmarkChecked = styled(Checkmark)` + background-color: ${colors.gardenPink} !important; + border: none; + :after { + display: block; + } +` + interface Props { name: string + label: string checked?: boolean onChange: (event: ChangeEvent) => void } -const CheckBox: React.SFC = ({ name, onChange, checked = false }) => { +const CheckBox: React.SFC = ({ name, label, onChange, checked = false }) => { + const Mark = checked ? CheckmarkChecked : Checkmark return ( - + ) } diff --git a/dashboard/src/components/graph/index.tsx b/dashboard/src/components/graph/index.tsx index 691b1ef6e5..4c7f856e0c 100644 --- a/dashboard/src/components/graph/index.tsx +++ b/dashboard/src/components/graph/index.tsx @@ -25,6 +25,7 @@ import Card from "../card" import "./graph.scss" import { colors, fontMedium } from "../../styles/variables" import Spinner from "../spinner" +import CheckBox from "../checkbox" interface Node { name: string @@ -303,18 +304,17 @@ class Chart extends Component { return ( -
-
+
+
{taskTypes.map(type => ( - +
))}
{ display: flex; justify-content: space-between; `, "ml-1 mr-1 pb-1")}> -
+
{status} {spinner}
diff --git a/dashboard/src/components/logs.tsx b/dashboard/src/components/logs.tsx index 8414308463..6a886da00d 100644 --- a/dashboard/src/components/logs.tsx +++ b/dashboard/src/components/logs.tsx @@ -6,9 +6,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import cls from "classnames" +import { css } from "emotion/macro" import styled from "@emotion/styled/macro" import { max } from "lodash" import React, { Component } from "react" +import Select from "react-select" import Terminal from "./terminal" import { FetchConfigResponse, FetchLogsResponse } from "../api/types" @@ -24,7 +27,8 @@ interface Props { } interface State { - selectedService: string + loading: boolean + selectedService: { value: string, label: string } } const Header = styled.div` @@ -32,15 +36,58 @@ const Header = styled.div` justify-content: space-between; ` +const Button = styled.div` + padding: 0.5em; + border-radius: 10%; + border: 1px solid ${colors.darkGray}; + cursor: pointer; + :hover { + background-color: ${colors.gray}; + transition: all 0.3s ease-out; + } + :active { + opacity: 0.5; + } +` + const Icon = styled.i` - color: ${colors.gardenPink}; font-size: 1.5rem; - cursor: pointer; :active { - color: ${colors.gardenPinkLighten(0.7)} + 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) => ({ + ...base, + "boxShadow": state.isFocused ? `0 0 0 1px ${colors.gray}` : 0, // The box shadow adds width to the border + "borderColor": state.isFocused ? colors.gray : base.borderColor, + "&:hover": { + borderColor: state.isFocused ? colors.gray : base.borderColor, + }, + }), + option: (base, state) => ({ + ...base, + color: colors.lightBlack, + backgroundColor: state.isSelected + ? colors.gray + : state.isFocused ? colors.lightGray : colors.white, + }), +} + class Logs extends Component { constructor(props) { @@ -48,52 +95,65 @@ class Logs extends Component { // TODO Use tab id instead of title this.state = { - selectedService: "all", + loading: false, + selectedService: { value: "all", label: "All service logs" }, } this.handleChange = this.handleChange.bind(this) this.refresh = this.refresh.bind(this) } - handleChange(event) { - this.setState({ selectedService: event.target.value }) + handleChange(selectedService) { + this.setState({ selectedService }) + } + + componentDidUpdate(_, prevState) { + if (prevState.loading) { + this.setState({ loading: false }) + } } refresh() { this.props.loadLogs(getServiceNames(this.props.config.moduleConfigs), true) + this.setState({ loading: true }) } render() { const { config, logs } = this.props - const { selectedService } = this.state + const { loading, selectedService } = this.state const serviceNames = getServiceNames(config.moduleConfigs) const maxServiceName = max(serviceNames).length - const title = selectedService === "all" - ? "All service logs" - : `${selectedService} logs` - const filteredLogs = selectedService === "all" - ? logs - : logs.filter(l => l.serviceName === selectedService) + const options = [{ value: "all", label: "All service logs" }] + .concat(serviceNames.map(name => ({ value: name, label: name }))) + + const { value, label } = selectedService + const title = value === "all" ? label : `${label} logs` + const filteredLogs = value === "all" ? logs : logs.filter(l => l.serviceName === value) + + const IconComp = loading ? IconLoading : Icon + return (
-
- +
+