diff --git a/web/public/index.html b/web/public/index.html index fed377ac05..b04b050cc9 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -4,6 +4,7 @@ + Tilt diff --git a/web/src/ApiButton.tsx b/web/src/ApiButton.tsx new file mode 100644 index 0000000000..056638d346 --- /dev/null +++ b/web/src/ApiButton.tsx @@ -0,0 +1,63 @@ +import { Icon } from "@material-ui/core" +import moment from "moment" +import React, { useState } from "react" +import { InstrumentedButton } from "./instrumentedComponents" + +type UIButton = Proto.v1alpha1UIButton + +type ApiButtonProps = { className?: string; button: UIButton } + +export const ApiButton: React.FC = (props) => { + const [loading, setLoading] = useState(false) + const onClick = async () => { + const toUpdate = { + metadata: { ...props.button.metadata }, + status: { ...props.button.status }, + } as UIButton + // apiserver's date format time is _extremely_ strict to the point that it requires the full + // six-decimal place microsecond precision, e.g. .000Z will be rejected, it must be .000000Z + // so use an explicit RFC3339 moment format to ensure it passes + toUpdate.status!.lastClickedAt = moment().format( + "YYYY-MM-DDTHH:mm:ss.SSSSSSZ" + ) + + // TODO(milas): currently the loading state just disables the button for the duration of + // the AJAX request to avoid duplicate clicks - there is no progress tracking at the + // moment, so there's no fancy spinner animation or propagation of result of action(s) + // that occur as a result of click right now + setLoading(true) + const url = `/proxy/apis/tilt.dev/v1alpha1/uibuttons/${ + toUpdate.metadata!.name + }/status` + try { + await fetch(url, { + method: "PUT", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(toUpdate), + }) + } finally { + setLoading(false) + } + } + // button text is not included in analytics name since that can be user data + return ( + + {props.children || ( + <> + {props.button.spec?.iconName && ( + {props.button.spec?.iconName} + )} + {props.button.spec?.text ?? "Button"} + + )} + + ) +} diff --git a/web/src/CustomNav.tsx b/web/src/CustomNav.tsx new file mode 100644 index 0000000000..3976e41932 --- /dev/null +++ b/web/src/CustomNav.tsx @@ -0,0 +1,37 @@ +import { Icon } from "@material-ui/core" +import React from "react" +import styled from "styled-components" +import { ApiButton } from "./ApiButton" +import { MenuButtonLabel, MenuButtonMixin } from "./GlobalNav" + +type CustomNavProps = { + view: Proto.webviewView +} + +const CustomNavButton = styled(ApiButton)` + ${MenuButtonMixin} + .apibtn-label { + display: none; + } +` + +export function CustomNav(props: CustomNavProps) { + const buttons = + props.view.uiButtons?.filter( + (b) => + b.spec?.location && + (b.spec.location.componentType ?? "").toLowerCase() === "global" && + (b.spec.location.componentID ?? "").toLowerCase() === "nav" + ) || [] + + return ( + + {buttons.map((b) => ( + + {b.spec?.iconName} + {b.spec?.text} + + ))} + + ) +} diff --git a/web/src/GlobalNav.tsx b/web/src/GlobalNav.tsx index b49aa221e8..b3d0d93375 100644 --- a/web/src/GlobalNav.tsx +++ b/web/src/GlobalNav.tsx @@ -25,7 +25,16 @@ export const GlobalNavRoot = styled.div` display: flex; align-items: stretch; ` -export const MenuButton = styled.button` +export const MenuButtonLabel = styled.div` + position: absolute; + bottom: 0; + font-size: ${FontSize.smallest}; + color: ${Color.blueDark}; + width: 200%; + transition: opacity ${AnimDuration.default} ease; + opacity: 0; +` +export const MenuButtonMixin = ` ${mixinResetButtonStyle}; display: flex; flex-direction: column; @@ -55,21 +64,14 @@ export const MenuButton = styled.button` pointer-events: none; cursor: default; } -` -const MenuButtonLabel = styled.div` - position: absolute; - bottom: 0; - font-size: ${FontSize.smallest}; - color: ${Color.blueDark}; - width: 200%; - transition: opacity ${AnimDuration.default} ease; - opacity: 0; - - ${MenuButton}:hover &, - ${MenuButton}[data-open="true"] & { + + &:hover ${MenuButtonLabel}, &[data-open="true"] ${MenuButtonLabel} { opacity: 1; } ` +export const MenuButton = styled.button` + ${MenuButtonMixin} +` const UpdateAvailableFloatIcon = styled(UpdateAvailableIcon)` display: none; position: absolute; diff --git a/web/src/HeaderBar.tsx b/web/src/HeaderBar.tsx index e86c33aa24..3154943459 100644 --- a/web/src/HeaderBar.tsx +++ b/web/src/HeaderBar.tsx @@ -2,6 +2,7 @@ import React from "react" import { Link } from "react-router-dom" import styled from "styled-components" import { ReactComponent as LogoWordmarkSvg } from "./assets/svg/logo-wordmark.svg" +import { CustomNav } from "./CustomNav" import { GlobalNav } from "./GlobalNav" import { usePathBuilder } from "./PathBuilder" import { @@ -93,6 +94,7 @@ export default function HeaderBar(props: HeaderBarProps) { All Resources + ) diff --git a/web/src/OverviewActionBar.tsx b/web/src/OverviewActionBar.tsx index 0d69f0272b..b109da4eb6 100644 --- a/web/src/OverviewActionBar.tsx +++ b/web/src/OverviewActionBar.tsx @@ -10,12 +10,12 @@ import { PopoverOrigin } from "@material-ui/core/Popover" import { makeStyles } from "@material-ui/core/styles" import ExpandMoreIcon from "@material-ui/icons/ExpandMore" import { History } from "history" -import moment from "moment" import React, { ChangeEvent, useEffect, useRef, useState } from "react" import { useHistory, useLocation } from "react-router" import styled from "styled-components" import { Alert } from "./alerts" import { incr } from "./analytics" +import { ApiButton } from "./ApiButton" import { ReactComponent as AlertSvg } from "./assets/svg/alert.svg" import { ReactComponent as CheckmarkSvg } from "./assets/svg/checkmark.svg" import { ReactComponent as CloseSvg } from "./assets/svg/close.svg" @@ -176,7 +176,7 @@ function FilterSourceMenu(props: FilterSourceMenuProps) { ) } -let ButtonRoot = styled(InstrumentedButton)` +const OverviewButtonMixin = ` font-family: ${Font.sansSerif}; display: flex; align-items: center; @@ -237,6 +237,14 @@ let ButtonRoot = styled(InstrumentedButton)` } ` +const ButtonRoot = styled(InstrumentedButton)` + ${OverviewButtonMixin} +` + +const CustomActionButton = styled(ApiButton)` + ${OverviewButtonMixin} +` + const WidgetRoot = styled.div` display: flex; ${ButtonRoot} + ${ButtonRoot} { @@ -673,53 +681,6 @@ function openEndpointUrl(url: string) { window.open(url, url) } -function ApiButton(props: { button: UIButton }) { - const [loading, setLoading] = useState(false) - const onClick = async () => { - const toUpdate = { - metadata: { ...props.button.metadata }, - status: { ...props.button.status }, - } as UIButton - // apiserver's date format time is _extremely_ strict to the point that it requires the full - // six-decimal place microsecond precision, e.g. .000Z will be rejected, it must be .000000Z - // so use an explicit RFC3339 moment format to ensure it passes - toUpdate.status!.lastClickedAt = moment().format( - "YYYY-MM-DDTHH:mm:ss.SSSSSSZ" - ) - - // TODO(milas): currently the loading state just disables the button for the duration of - // the AJAX request to avoid duplicate clicks - there is no progress tracking at the - // moment, so there's no fancy spinner animation or propagation of result of action(s) - // that occur as a result of click right now - setLoading(true) - const url = `/proxy/apis/tilt.dev/v1alpha1/uibuttons/${ - toUpdate.metadata!.name - }/status` - try { - await fetch(url, { - method: "PUT", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify(toUpdate), - }) - } finally { - setLoading(false) - } - } - // button text is not included in analytics name since that can be user data - return ( - - {props.button.spec?.text ?? "Button"} - - ) -} - export function OverviewWidgets(props: { buttons?: UIButton[] }) { if (!props.buttons?.length) { return null @@ -728,7 +689,7 @@ export function OverviewWidgets(props: { buttons?: UIButton[] }) { return ( {props.buttons?.map((b) => ( - + ))} ) diff --git a/web/src/instrumentedComponents.tsx b/web/src/instrumentedComponents.tsx index 42d4fe5b3c..a543098a41 100644 --- a/web/src/instrumentedComponents.tsx +++ b/web/src/instrumentedComponents.tsx @@ -1,4 +1,4 @@ -import { LegacyRef } from "react" +import React, { LegacyRef } from "react" import { incr, Tags } from "./analytics" export type InstrumentedButtonProps = React.ButtonHTMLAttributes & { diff --git a/web/src/view.d.ts b/web/src/view.d.ts index 686cfef38a..d989a51263 100644 --- a/web/src/view.d.ts +++ b/web/src/view.d.ts @@ -484,9 +484,11 @@ declare namespace Proto { */ iconName?: string; /** - * IconSVG is an SVG to use as the icon to appear next to button text or on the button itself (dependong on button + * IconSVG is an SVG to use as the icon to appear next to button text or on the button itself (depending on button * location). * + * This should be an element scaled for a 24x24 viewport. + * * If both IconSVG and IconName are specified, IconSVG will take precedence. * * +optional