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