From 1a71958f2045d700babe11d158a1a0b19dd65d02 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Wed, 20 Mar 2024 13:32:02 +0000 Subject: [PATCH 01/17] First pass at mobile styles --- app/components/EquivalentCliCommand.tsx | 7 +- app/components/Sidebar.tsx | 144 +++++++++++++++++- app/components/Terminal.tsx | 7 +- app/components/TopBar.tsx | 66 +++++++- app/components/TopBarPicker.tsx | 16 +- .../form/fields/DateTimeRangePicker.tsx | 4 +- .../form/fields/DisksTableField.tsx | 10 +- app/hooks/use-menu-state.ts | 40 +++++ app/hooks/use-window-size.ts | 33 ++++ app/layouts/SettingsLayout.tsx | 15 +- app/layouts/helpers.tsx | 12 +- .../instances/instance/InstancePage.tsx | 2 +- .../instances/instance/SerialConsolePage.tsx | 2 +- .../instances/instance/tabs/MetricsTab.tsx | 17 +-- .../instances/instance/tabs/StorageTab.tsx | 4 +- app/pages/project/vpcs/VpcPage/VpcPage.tsx | 2 +- app/pages/system/inventory/sled/SledPage.tsx | 2 +- app/pages/system/silos/SiloPage.tsx | 2 +- app/ui/lib/DateRangePicker.tsx | 8 +- app/ui/lib/Modal.tsx | 2 +- app/ui/lib/PageHeader.tsx | 2 +- app/ui/lib/PropertiesTable.tsx | 6 +- app/ui/lib/Radio.tsx | 4 +- app/ui/lib/SideModal.tsx | 2 +- app/ui/lib/Toast.tsx | 2 +- app/ui/styles/components/Tabs.css | 15 +- app/ui/styles/components/properties-table.css | 8 + app/ui/styles/components/side-modal.css | 6 + app/ui/styles/components/table.css | 6 +- app/ui/styles/index.css | 10 ++ index.html | 6 +- tailwind.config.js | 13 +- 32 files changed, 393 insertions(+), 82 deletions(-) create mode 100644 app/hooks/use-menu-state.ts create mode 100644 app/hooks/use-window-size.ts diff --git a/app/components/EquivalentCliCommand.tsx b/app/components/EquivalentCliCommand.tsx index 9aa882f6af..9fb01aa879 100644 --- a/app/components/EquivalentCliCommand.tsx +++ b/app/components/EquivalentCliCommand.tsx @@ -31,7 +31,12 @@ export function EquivalentCliCommand({ command }: { command: string }) { return ( <> - diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx index e4b383282b..44d4f2dbd2 100644 --- a/app/components/Sidebar.tsx +++ b/app/components/Sidebar.tsx @@ -5,14 +5,26 @@ * * Copyright Oxide Computer Company */ +import * as Dialog from '@radix-ui/react-dialog' +import { animated, useTransition } from '@react-spring/web' import cn from 'classnames' -import { NavLink } from 'react-router-dom' +import { NavLink, useLocation } from 'react-router-dom' -import { Action16Icon, Document16Icon } from '@oxide/design-system/icons/react' +import { + Action16Icon, + Document16Icon, + Key16Icon, + Profile16Icon, +} from '@oxide/design-system/icons/react' +import { navToLogin, useApiMutation } from '~/api' import { openQuickActions } from '~/hooks' +import { closeSidebar, useMenuState } from '~/hooks/use-menu-state' +import { useCurrentUser } from '~/layouts/AuthenticatedLayout' import { Button } from '~/ui/lib/Button' +import { Divider } from '~/ui/lib/Divider' import { Truncate } from '~/ui/lib/Truncate' +import { pb } from '~/util/path-builder' const linkStyles = 'flex h-7 items-center rounded px-2 text-sans-md hover:bg-hover svg:mr-2 svg:text-quinary text-secondary' @@ -55,16 +67,113 @@ const JumpToButton = () => { } export function Sidebar({ children }: { children: React.ReactNode }) { + const AnimatedDialogContent = animated(Dialog.Content) + const { isOpen } = useMenuState() + const config = { tension: 1200, mass: 0.125 } + const { pathname } = useLocation() + + const transitions = useTransition(isOpen, { + from: { x: -50 }, + enter: { x: 0 }, + config: isOpen ? config : { duration: 0 }, + }) + return ( -
-
- -
- {children} + <> + {transitions( + ({ x }, item) => + item && ( + { + if (!open) closeSidebar() + }} + // https://github.com/radix-ui/primitives/issues/1159#issuecomment-1559813266 + modal={false} + > +
+ `translate3d(${value}%, 0px, 0px)`), + }} + forceMount + > +
+ +
+ {children} + {pathname.split('/')[1] !== 'settings' && } +
+ + ) + )} + + ) +} + +export const ProfileLinks = () => { + const { me } = useCurrentUser() + + const logout = useApiMutation('logout', { + onSuccess: () => { + // server will respond to /login with a login redirect + // TODO-usability: do we just want to dump them back to login or is there + // another page that would make sense, like a logged out homepage + navToLogin({ includeCurrent: false }) + }, + }) + + return ( +
+ + + + + Profile + + + SSH Keys + + logout.mutate({})}> + Sign out + +
) } +const SignOut16Icon = () => ( + + + + + +) + interface SidebarNav { children: React.ReactNode heading?: string @@ -105,3 +214,24 @@ export const NavLinkItem = (props: { ) + +export const NavButtonItem = (props: { + onClick: () => void + children: React.ReactNode + disabled?: boolean +}) => ( +
  • + +
  • +) diff --git a/app/components/Terminal.tsx b/app/components/Terminal.tsx index 3f27e80dad..ddddb0f5f8 100644 --- a/app/components/Terminal.tsx +++ b/app/components/Terminal.tsx @@ -104,8 +104,11 @@ export default function Terminal({ ws }: TerminalProps) { return ( <> -
    -
    +
    +
    term?.scrollToTop()}> diff --git a/app/components/TopBar.tsx b/app/components/TopBar.tsx index e787145265..082017850a 100644 --- a/app/components/TopBar.tsx +++ b/app/components/TopBar.tsx @@ -5,16 +5,19 @@ * * Copyright Oxide Computer Company */ +import cn from 'classnames' import React from 'react' import { useNavigate } from 'react-router-dom' import { navToLogin, useApiMutation } from '@oxide/api' import { + Close12Icon, DirectionDownIcon, Info16Icon, Profile16Icon, } from '@oxide/design-system/icons/react' +import { closeSidebar, openSidebar, useMenuState } from '~/hooks/use-menu-state' import { useCurrentUser } from '~/layouts/AuthenticatedLayout' import { Button, buttonStyle } from '~/ui/lib/Button' import { DropdownMenu } from '~/ui/lib/DropdownMenu' @@ -39,19 +42,45 @@ export function TopBar({ children }: { children: React.ReactNode }) { // picker is going to come in null when the user isn't supposed to see it const [cornerPicker, ...otherPickers] = React.Children.toArray(children) + const { isOpen } = useMenuState() + // The height of this component is governed by the `PageContainer` // It's important that this component returns two distinct elements (wrapped in a fragment). // Each element will occupy one of the top column slots provided by `PageContainer`. return ( - <> -
    +
    +
    + + {cornerPicker}
    + {/* Height is governed by PageContainer grid */} {/* shrink-0 is needed to prevent getting squished by body content */} -
    -
    -
    {otherPickers}
    +
    +
    +
    + {otherPickers} +
    @@ -72,7 +104,7 @@ export function TopBar({ children }: { children: React.ReactNode }) { size="sm" variant="secondary" aria-label="User menu" - className="ml-2" + className="ml-2 md-:hidden" innerClassName="space-x-2" > @@ -104,6 +136,24 @@ export function TopBar({ children }: { children: React.ReactNode }) {
    - +
    ) } + +const Menu12Icon = ({ className }: { className: string }) => ( + + + +) diff --git a/app/components/TopBarPicker.tsx b/app/components/TopBarPicker.tsx index a4a2f757bb..dec2a54bda 100644 --- a/app/components/TopBarPicker.tsx +++ b/app/components/TopBarPicker.tsx @@ -50,7 +50,7 @@ const TopBarPicker = (props: TopBarPickerProps) => { // left corner picker. The separator starts from the leftmost of "other // pickers". But the leftmost corner one is in its own container and // therefore always last-of-type, so it will never get one. - className="after:text-mono-lg flex w-full items-center justify-between after:mx-4 after:content-['/'] after:text-quinary last-of-type:after:content-none" + className="flex w-full items-center justify-between" > {props.current ? ( { /> } > -
    +
    {props.icon ? ( -
    {props.icon}
    +
    {props.icon}
    ) : null} -
    + {/* If it has an icon it must be a silo picker and has specific styling on mobile */} +
    {props.category}
    {props.display ?? props.current} @@ -81,7 +87,7 @@ const TopBarPicker = (props: TopBarPickerProps) => { > {props.icon ?
    {props.icon}
    : null} -
    +
    Select
    {props.category} diff --git a/app/components/form/fields/DateTimeRangePicker.tsx b/app/components/form/fields/DateTimeRangePicker.tsx index 291f7c7020..805958269f 100644 --- a/app/components/form/fields/DateTimeRangePicker.tsx +++ b/app/components/form/fields/DateTimeRangePicker.tsx @@ -101,9 +101,9 @@ export function DateTimeRangePicker({ onRangeChange, }: DateTimeRangePickerProps) { return ( -
    + )} -
    - diff --git a/app/hooks/use-menu-state.ts b/app/hooks/use-menu-state.ts new file mode 100644 index 0000000000..f80adea6b3 --- /dev/null +++ b/app/hooks/use-menu-state.ts @@ -0,0 +1,40 @@ +import { useEffect } from 'react' +import { useLocation } from 'react-router-dom' +import { create } from 'zustand' +import { shallow } from 'zustand/shallow' + +import { useWindowSize } from './use-window-size' + +type StoreState = { + isOpen: boolean +} + +const useStore = create(() => ({ isOpen: false })) + +export function openSidebar() { + useStore.setState({ isOpen: true }) +} + +export function closeSidebar() { + useStore.setState({ isOpen: false }) +} + +export function toggleSidebar() { + useStore.setState((state) => ({ isOpen: !state.isOpen })) +} + +export function useMenuState() { + const { size } = useWindowSize() + const location = useLocation() + + useEffect(() => { + closeSidebar() + }, [location.pathname]) + + return useStore( + (store) => ({ + isOpen: size.width >= 1024 ? true : store.isOpen, + }), + shallow + ) +} diff --git a/app/hooks/use-window-size.ts b/app/hooks/use-window-size.ts new file mode 100644 index 0000000000..448ed8bd18 --- /dev/null +++ b/app/hooks/use-window-size.ts @@ -0,0 +1,33 @@ +import { useEffect, useState } from 'react' + +export const useWindowSize = () => { + const [size, setSize] = useState<{ + width: number + height: number + }>({ + width: 0, + height: 0, + }) + + function handleResize() { + setSize({ + width: window.innerWidth, + height: window.innerHeight, + }) + } + + useEffect(() => { + // Only execute all the code below in client side + if (typeof window !== 'undefined') { + window.addEventListener('resize', handleResize) + + handleResize() + + return () => window.removeEventListener('resize', handleResize) + } + }, []) + + return { + size, + } +} diff --git a/app/layouts/SettingsLayout.tsx b/app/layouts/SettingsLayout.tsx index 4704399197..9362e858d4 100644 --- a/app/layouts/SettingsLayout.tsx +++ b/app/layouts/SettingsLayout.tsx @@ -8,15 +8,14 @@ import { useMemo } from 'react' import { useLocation, useNavigate } from 'react-router-dom' -import { Folder16Icon, Key16Icon, Profile16Icon } from '@oxide/design-system/icons/react' +import { Folder16Icon } from '@oxide/design-system/icons/react' import { TopBar } from '~/components/TopBar' import { SiloSystemPicker } from '~/components/TopBarPicker' import { useQuickActions } from '~/hooks' -import { Divider } from '~/ui/lib/Divider' import { pb } from '~/util/path-builder' -import { DocsLinkItem, NavLinkItem, Sidebar } from '../components/Sidebar' +import { DocsLinkItem, NavLinkItem, ProfileLinks, Sidebar } from '../components/Sidebar' import { ContentPane, PageContainer } from './helpers' export function SettingsLayout() { @@ -53,15 +52,7 @@ export function SettingsLayout() { - - - - Profile - - - SSH Keys - - + diff --git a/app/layouts/helpers.tsx b/app/layouts/helpers.tsx index 28edb2e193..e61ec6b68f 100644 --- a/app/layouts/helpers.tsx +++ b/app/layouts/helpers.tsx @@ -14,14 +14,18 @@ import { useScrollRestoration } from '~/hooks/use-scroll-restoration' import { SkipLinkTarget } from '~/ui/lib/SkipLink' import { classed } from '~/util/classed' -export const PageContainer = classed.div`grid h-screen grid-cols-[14.25rem,1fr] grid-rows-[60px,1fr]` +export const PageContainer = classed.div`grid h-[100dvh] lg+:grid-cols-[14.25rem,auto] grid-rows-[60px,1fr]` export function ContentPane() { const ref = useRef(null) useScrollRestoration(ref) return ( -
    -
    +
    +
    @@ -42,7 +46,7 @@ export function ContentPane() { * `
    ` because we don't need it. */ export const SerialConsoleContentPane = () => ( -
    +
    diff --git a/app/pages/project/instances/instance/InstancePage.tsx b/app/pages/project/instances/instance/InstancePage.tsx index b3c5db3760..d0e91162ba 100644 --- a/app/pages/project/instances/instance/InstancePage.tsx +++ b/app/pages/project/instances/instance/InstancePage.tsx @@ -139,7 +139,7 @@ export function InstancePage() { }>{instance.name} - + {instance.ncpus} diff --git a/app/pages/project/instances/instance/SerialConsolePage.tsx b/app/pages/project/instances/instance/SerialConsolePage.tsx index 9d80e0b4d6..cd12033afc 100644 --- a/app/pages/project/instances/instance/SerialConsolePage.tsx +++ b/app/pages/project/instances/instance/SerialConsolePage.tsx @@ -113,7 +113,7 @@ export function SerialConsolePage() { {ws.current && }
    -
    +
    diff --git a/app/pages/project/instances/instance/tabs/MetricsTab.tsx b/app/pages/project/instances/instance/tabs/MetricsTab.tsx index a80f29a63d..f29d3cd492 100644 --- a/app/pages/project/instances/instance/tabs/MetricsTab.tsx +++ b/app/pages/project/instances/instance/tabs/MetricsTab.tsx @@ -123,14 +123,13 @@ function DiskMetric({ } return ( -
    -

    +
    +

    {title}
    {label}
    {isLoading && }

    }> -
    +
    -
    +
    {/* see the following link for the source of truth on what these mean https://github.com/oxidecomputer/crucible/blob/258f162b/upstairs/src/stats.rs#L9-L50 */} -
    +
    -
    +
    -
    +
    diff --git a/app/pages/project/instances/instance/tabs/StorageTab.tsx b/app/pages/project/instances/instance/tabs/StorageTab.tsx index 0dfd515472..ba0d5f3dcc 100644 --- a/app/pages/project/instances/instance/tabs/StorageTab.tsx +++ b/app/pages/project/instances/instance/tabs/StorageTab.tsx @@ -175,12 +175,13 @@ export function StorageTab() {
    -
    +
    @@ -190,6 +191,7 @@ export function StorageTab() { onClick={() => setShowDiskAttach(true)} disabledReason={<>Instance must be {attachableStates} to attach a disk} disabled={!instanceCan.attachDisk(instance)} + className="md-:w-full" > Attach existing disk diff --git a/app/pages/project/vpcs/VpcPage/VpcPage.tsx b/app/pages/project/vpcs/VpcPage/VpcPage.tsx index 8536684422..9794267933 100644 --- a/app/pages/project/vpcs/VpcPage/VpcPage.tsx +++ b/app/pages/project/vpcs/VpcPage/VpcPage.tsx @@ -47,7 +47,7 @@ export function VpcPage() { }>{vpc.name} - + {vpc.description || } diff --git a/app/pages/system/inventory/sled/SledPage.tsx b/app/pages/system/inventory/sled/SledPage.tsx index c454b5f97d..581c583b1f 100644 --- a/app/pages/system/inventory/sled/SledPage.tsx +++ b/app/pages/system/inventory/sled/SledPage.tsx @@ -37,7 +37,7 @@ export function SledPage() { }>Sled - + {sled.id} diff --git a/app/pages/system/silos/SiloPage.tsx b/app/pages/system/silos/SiloPage.tsx index 91c8f2f9f8..ba485c9d8e 100644 --- a/app/pages/system/silos/SiloPage.tsx +++ b/app/pages/system/silos/SiloPage.tsx @@ -55,7 +55,7 @@ export function SiloPage() { }>{silo.name} - + {silo.id} diff --git a/app/ui/lib/DateRangePicker.tsx b/app/ui/lib/DateRangePicker.tsx index b303331a19..87bdb9258b 100644 --- a/app/ui/lib/DateRangePicker.tsx +++ b/app/ui/lib/DateRangePicker.tsx @@ -68,7 +68,11 @@ export function DateRangePicker(props: DateRangePickerProps) { : 'border-default ring-accent-secondary' )} > -
    +
    {label} {state.isInvalid && (
    @@ -76,7 +80,7 @@ export function DateRangePicker(props: DateRangePickerProps) {
    )}
    -
    +
    diff --git a/app/ui/lib/Modal.tsx b/app/ui/lib/Modal.tsx index a5cd7a477f..d1aaff7756 100644 --- a/app/ui/lib/Modal.tsx +++ b/app/ui/lib/Modal.tsx @@ -59,7 +59,7 @@ export function Modal({ children, onDismiss, title, isOpen }: ModalProps) { `translate3d(-50%, ${-50 + value}%, 0px)`), diff --git a/app/ui/lib/PageHeader.tsx b/app/ui/lib/PageHeader.tsx index 89f11ead05..210ebc5839 100644 --- a/app/ui/lib/PageHeader.tsx +++ b/app/ui/lib/PageHeader.tsx @@ -9,7 +9,7 @@ import type { ReactElement } from 'react' import { classed } from '~/util/classed' -export const PageHeader = classed.header`mb-16 mt-12 flex items-center justify-between` +export const PageHeader = classed.header`mb-16 md-:mt-8 mt-12 flex items-center justify-between` interface PageTitleProps { icon?: ReactElement diff --git a/app/ui/lib/PropertiesTable.tsx b/app/ui/lib/PropertiesTable.tsx index 58590a2feb..6ea9c3f71c 100644 --- a/app/ui/lib/PropertiesTable.tsx +++ b/app/ui/lib/PropertiesTable.tsx @@ -26,7 +26,7 @@ export function PropertiesTable({ className, children }: PropertiesTableProps) {
    {children} @@ -43,7 +43,7 @@ PropertiesTable.Row = ({ label, children }: PropertiesTableRowProps) => ( {label} -
    +
    {children}
    @@ -63,7 +63,7 @@ PropertiesTable.Group = ({ children, className }: PropertiesTableGroupProps) =>
    {children} diff --git a/app/ui/lib/Radio.tsx b/app/ui/lib/Radio.tsx index 777b6c21f7..e2d62b8a8b 100644 --- a/app/ui/lib/Radio.tsx +++ b/app/ui/lib/Radio.tsx @@ -53,13 +53,15 @@ const cardLabelStyles = ` peer-disabled:bg-disabled peer-disabled:peer-checked:bg-accent-secondary peer-checked:peer-disabled:hover:border-accent-secondary peer-disabled:hover:border-default peer-disabled:[&>*_.text-secondary]:text-disabled peer-disabled:text-disabled peer-disabled:peer-checked:text-accent-disabled peer-disabled:peer-checked:[&>*_.text-secondary]:text-accent-disabled + + md-:w-full ` export function RadioCard({ children, className, ...inputProps }: RadioProps) { // HACK: This forces the focus states for storybook stories const focus = className?.includes(':focus') ? ':focus' : '' return ( -

    -
    +
    @@ -46,7 +46,7 @@ export function ContentPane() { * `
    ` because we don't need it. */ export const SerialConsoleContentPane = () => ( -
    +
    diff --git a/app/ui/styles/components/Tabs.css b/app/ui/styles/components/Tabs.css index 69607d38e3..dbc0977185 100644 --- a/app/ui/styles/components/Tabs.css +++ b/app/ui/styles/components/Tabs.css @@ -16,8 +16,12 @@ scrollbar-width: none; -webkit-overflow-scrolling: touch; position: sticky; - top: 1px; - @apply z-topBar bg-default; + top: var(--navigation-height); + @apply z-popover mb-0 bg-default; + } + + .ox-tabs .ox-tabs-list + * { + @apply mt-8; } } @@ -26,7 +30,7 @@ } .ox-tabs-list { - @apply mb-8 flex bg-transparent; + @apply flex bg-transparent; } .ox-tabs-list:after { diff --git a/app/ui/styles/index.css b/app/ui/styles/index.css index 53e2629bb0..863277b582 100644 --- a/app/ui/styles/index.css +++ b/app/ui/styles/index.css @@ -44,6 +44,8 @@ :root { --content-gutter: 2.5rem; + --sidebar-width: 14.25rem; + --navigation-height: 3.75rem; } @media (max-width: 768px) { @@ -53,12 +55,13 @@ } @layer base { - html { + html, + body { overscroll-behavior-y: none; } body { - @apply overflow-y-hidden text-default bg-default; + @apply text-default bg-default; font-family: SuisseIntl, -apple-system, From e55af0c7f08ae1b1587cbc5ff56dd8288942fccf Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Wed, 20 Mar 2024 17:50:27 +0000 Subject: [PATCH 03/17] Update Tabs.css --- app/ui/styles/components/Tabs.css | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/ui/styles/components/Tabs.css b/app/ui/styles/components/Tabs.css index dbc0977185..96ccc3aff9 100644 --- a/app/ui/styles/components/Tabs.css +++ b/app/ui/styles/components/Tabs.css @@ -19,10 +19,12 @@ top: var(--navigation-height); @apply z-popover mb-0 bg-default; } +} - .ox-tabs .ox-tabs-list + * { - @apply mt-8; - } +/* We apply this to the next item not the tab bar so that +the sticky tab bar finishes at the correct point */ +.ox-tabs .ox-tabs-list + * { + @apply mt-8; } .ox-tabs.full-width .ox-tabs-panel { From 2dc1fda34dfeb9bda9610917e0a025d53acddccb Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Wed, 20 Mar 2024 17:51:28 +0000 Subject: [PATCH 04/17] We can use the ID now --- test/e2e/scroll-restore.e2e.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/scroll-restore.e2e.ts b/test/e2e/scroll-restore.e2e.ts index 318b2e8a59..eae2eb0291 100644 --- a/test/e2e/scroll-restore.e2e.ts +++ b/test/e2e/scroll-restore.e2e.ts @@ -8,13 +8,13 @@ import { expect, test, type Page } from './utils' async function expectScrollTop(page: Page, expected: number) { - const container = page.getByTestId('scroll-container') + const container = page.locator('#content_pane') const getScrollTop = () => container.evaluate((el: HTMLElement) => el.scrollTop) await expect.poll(getScrollTop).toBe(expected) } async function scrollTo(page: Page, to: number) { - const container = page.getByTestId('scroll-container') + const container = page.locator('#content_pane') await container.evaluate((el: HTMLElement, to) => el.scrollTo(0, to), to) } From fe6b9ba461eab3f51ae0b1dd262ddf2d59a85518 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Thu, 21 Mar 2024 11:34:09 +0000 Subject: [PATCH 05/17] Tweak msw banner --- app/components/MswBanner.tsx | 14 +++++++++++--- app/layouts/helpers.tsx | 6 +++--- .../instances/instance/tabs/MetricsTab.tsx | 2 +- app/ui/styles/index.css | 16 +++++++++++++++- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/app/components/MswBanner.tsx b/app/components/MswBanner.tsx index 82b943d95e..7dce3488e3 100644 --- a/app/components/MswBanner.tsx +++ b/app/components/MswBanner.tsx @@ -5,7 +5,7 @@ * * Copyright Oxide Computer Company */ -import { useState, type ReactNode } from 'react' +import { useEffect, useState, type ReactNode } from 'react' import { Info16Icon, NextArrow12Icon } from '@oxide/design-system/icons/react' @@ -29,10 +29,18 @@ function ExternalLink({ href, children }: { href: string; children: ReactNode }) export function MswBanner() { const [isOpen, setIsOpen] = useState(false) const closeModal = () => setIsOpen(false) + + useEffect(() => { + document.body.classList.add('msw-banner') + + return () => { + document.body.classList.remove('msw-banner') + } + }, []) + return ( <> - {/* The [&+*]:pt-10 style is to ensure the page container isn't pushed out of screen as it uses 100vh for layout */} -
    -
    +
    diff --git a/app/pages/project/instances/instance/tabs/MetricsTab.tsx b/app/pages/project/instances/instance/tabs/MetricsTab.tsx index f29d3cd492..49649dae83 100644 --- a/app/pages/project/instances/instance/tabs/MetricsTab.tsx +++ b/app/pages/project/instances/instance/tabs/MetricsTab.tsx @@ -198,7 +198,7 @@ export function MetricsTab() { <>
    Date: Thu, 21 Mar 2024 12:53:18 +0000 Subject: [PATCH 06/17] More tablet and mobile improvements --- app/components/ErrorPage.tsx | 4 ++-- app/components/RefetchIntervalPicker.tsx | 4 ++-- app/components/Sidebar.tsx | 2 +- app/forms/firewall-rules-create.tsx | 5 ++++- app/layouts/helpers.tsx | 2 +- app/pages/system/UtilizationPage.tsx | 6 +++--- app/ui/lib/DateRangePicker.tsx | 2 +- app/ui/lib/DropdownMenu.tsx | 1 + app/ui/styles/components/Tabs.css | 10 ++++------ app/ui/styles/index.css | 4 ---- 10 files changed, 19 insertions(+), 21 deletions(-) diff --git a/app/components/ErrorPage.tsx b/app/components/ErrorPage.tsx index 36cf2d0b97..7aac7cc355 100644 --- a/app/components/ErrorPage.tsx +++ b/app/components/ErrorPage.tsx @@ -32,11 +32,11 @@ export function ErrorPage({ children }: Props) { to="/" className="flex items-center p-6 text-mono-sm text-secondary hover:text-default" > - + Back to console
    -
    +
    diff --git a/app/components/RefetchIntervalPicker.tsx b/app/components/RefetchIntervalPicker.tsx index f874c400a8..eb24079eeb 100644 --- a/app/components/RefetchIntervalPicker.tsx +++ b/app/components/RefetchIntervalPicker.tsx @@ -54,7 +54,7 @@ export function useIntervalPicker({ enabled, isLoading, fn }: Props) { intervalMs: (enabled && intervalPresets[intervalPreset]) || undefined, intervalPicker: (
    -
    +
    Refreshed {format(lastFetched, 'HH:mm')}
    @@ -73,7 +73,7 @@ export function useIntervalPicker({ enabled, isLoading, fn }: Props) { { }) return ( -
    +
    diff --git a/app/forms/firewall-rules-create.tsx b/app/forms/firewall-rules-create.tsx index 43afafdf1e..23b9ee15a7 100644 --- a/app/forms/firewall-rules-create.tsx +++ b/app/forms/firewall-rules-create.tsx @@ -32,6 +32,7 @@ import { useForm, useVpcSelector } from '~/hooks' import { Badge } from '~/ui/lib/Badge' import { Button } from '~/ui/lib/Button' import { FormDivider } from '~/ui/lib/Divider' +import { FieldLabel } from '~/ui/lib/FieldLabel' import * as MiniTable from '~/ui/lib/MiniTable' import { KEYS } from '~/ui/util/keys' @@ -448,7 +449,9 @@ export const CommonFields = ({ error, control }: CommonFieldsProps) => {
    - Protocols +
    + Protocols +
    TCP diff --git a/app/layouts/helpers.tsx b/app/layouts/helpers.tsx index ae75b911aa..5eed2b457b 100644 --- a/app/layouts/helpers.tsx +++ b/app/layouts/helpers.tsx @@ -31,7 +31,7 @@ export function ContentPane() {
    -
    +
    diff --git a/app/pages/system/UtilizationPage.tsx b/app/pages/system/UtilizationPage.tsx index 0a07b38f23..627a537637 100644 --- a/app/pages/system/UtilizationPage.tsx +++ b/app/pages/system/UtilizationPage.tsx @@ -103,10 +103,10 @@ const MetricsTab = () => { return ( <> -
    +
    { {intervalPicker} -
    +
    {label} diff --git a/app/ui/lib/DropdownMenu.tsx b/app/ui/lib/DropdownMenu.tsx index ddebab55a4..0050d9638e 100644 --- a/app/ui/lib/DropdownMenu.tsx +++ b/app/ui/lib/DropdownMenu.tsx @@ -28,6 +28,7 @@ export const DropdownMenu = { Content: forwardRef(({ className, ...props }: DropdownMenuContentProps, ref: DivRef) => ( e.preventDefault()} className={cn('DropdownMenuContent', className)} diff --git a/app/ui/styles/components/Tabs.css b/app/ui/styles/components/Tabs.css index 96ccc3aff9..43e0c494c8 100644 --- a/app/ui/styles/components/Tabs.css +++ b/app/ui/styles/components/Tabs.css @@ -15,15 +15,12 @@ overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; - position: sticky; - top: var(--navigation-height); - @apply z-popover mb-0 bg-default; } } /* We apply this to the next item not the tab bar so that the sticky tab bar finishes at the correct point */ -.ox-tabs .ox-tabs-list + * { +.ox-tabs > div:not(.ox-tabs-list) { @apply mt-8; } @@ -32,11 +29,12 @@ the sticky tab bar finishes at the correct point */ } .ox-tabs-list { - @apply flex bg-transparent; + top: var(--navigation-height); + @apply sticky z-popover flex bg-transparent bg-default; } .ox-tabs-list:after { - @apply block w-full border-b border-secondary md-:w-[var(--content-gutter)] md-:min-w-max md-:flex-shrink-0; + @apply block w-full flex-grow border-b border-secondary md-:w-[var(--content-gutter)] md-:min-w-max md-:flex-shrink-0; content: ' '; } .ox-tabs.full-width .ox-tabs-list:before { diff --git a/app/ui/styles/index.css b/app/ui/styles/index.css index f4255a814e..6a82841e91 100644 --- a/app/ui/styles/index.css +++ b/app/ui/styles/index.css @@ -66,10 +66,6 @@ @apply min-h-full; } - .msw-banner .pagination { - @apply bottom-10; - } - .msw-banner #content_pane { @apply pb-10; } From ea4caa0dfac0d25d803a6a804765788a0ce1106e Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Thu, 21 Mar 2024 14:39:32 +0000 Subject: [PATCH 07/17] Licenses --- app/hooks/use-menu-state.ts | 8 ++++++++ app/hooks/use-window-size.ts | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/app/hooks/use-menu-state.ts b/app/hooks/use-menu-state.ts index f80adea6b3..427dd2f14f 100644 --- a/app/hooks/use-menu-state.ts +++ b/app/hooks/use-menu-state.ts @@ -1,3 +1,11 @@ +/* + * 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 https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + import { useEffect } from 'react' import { useLocation } from 'react-router-dom' import { create } from 'zustand' diff --git a/app/hooks/use-window-size.ts b/app/hooks/use-window-size.ts index 448ed8bd18..a1c204509c 100644 --- a/app/hooks/use-window-size.ts +++ b/app/hooks/use-window-size.ts @@ -1,3 +1,11 @@ +/* + * 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 https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + import { useEffect, useState } from 'react' export const useWindowSize = () => { From 05dc53391338820f097e883d7818d0f4b7c0c991 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Thu, 21 Mar 2024 14:40:33 +0000 Subject: [PATCH 08/17] Add missing ID --- app/forms/firewall-rules-create.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/forms/firewall-rules-create.tsx b/app/forms/firewall-rules-create.tsx index 23b9ee15a7..6b0ef66ebe 100644 --- a/app/forms/firewall-rules-create.tsx +++ b/app/forms/firewall-rules-create.tsx @@ -450,7 +450,7 @@ export const CommonFields = ({ error, control }: CommonFieldsProps) => {
    - Protocols + Protocols
    From 0c6cd0dfadafd50c1ba22f2127b236977d820e3c Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Thu, 21 Mar 2024 17:18:51 +0000 Subject: [PATCH 09/17] Fix sidebar issues --- app/components/Sidebar.tsx | 89 +++++++++++++++++++++++-------------- app/components/TopBar.tsx | 7 ++- app/hooks/use-menu-state.ts | 6 ++- 3 files changed, 63 insertions(+), 39 deletions(-) diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx index 8f0708f3f6..a45db9cf90 100644 --- a/app/components/Sidebar.tsx +++ b/app/components/Sidebar.tsx @@ -68,7 +68,7 @@ const JumpToButton = () => { export function Sidebar({ children }: { children: React.ReactNode }) { const AnimatedDialogContent = animated(Dialog.Content) - const { isOpen } = useMenuState() + const { isOpen, isSmallScreen } = useMenuState() const config = { tension: 1200, mass: 0.125 } const { pathname } = useLocation() @@ -78,41 +78,64 @@ export function Sidebar({ children }: { children: React.ReactNode }) { config: isOpen ? config : { duration: 0 }, }) - return ( + const SidebarContent = () => ( <> - {transitions( - ({ x }, item) => - item && ( - { - if (!open) closeSidebar() - }} - // https://github.com/radix-ui/primitives/issues/1159#issuecomment-1559813266 - modal={false} - > -
    - `translate3d(${value}%, 0px, 0px)`), - }} - forceMount - > -
    - -
    - {children} - {pathname.split('/')[1] !== 'settings' && } -
    - - ) - )} +
    + +
    + {children} + {pathname.split('/')[1] !== 'settings' && } ) + + if (isSmallScreen) { + return ( + <> + {transitions( + ({ x }, item) => + item && ( + { + if (!open) closeSidebar() + }} + // Modal needs to be false to be able to click on top bar + modal={false} + > +
    + `translate3d(${value}%, 0px, 0px)`), + }} + forceMount + onInteractOutside={(e) => { + // We want to handle opening / closing with the menu button ourselves + // Not doing this can result in the two events fighting + if ((e.target as HTMLElement)?.title === 'Sidebar') { + e.preventDefault() + e.stopPropagation() + return null + } + }} + > + + + + ) + )} + + ) + } else { + return ( +
    + +
    + ) + } } export const ProfileLinks = () => { diff --git a/app/components/TopBar.tsx b/app/components/TopBar.tsx index 345fca01b2..493d7ebd12 100644 --- a/app/components/TopBar.tsx +++ b/app/components/TopBar.tsx @@ -50,15 +50,14 @@ export function TopBar({ children }: { children: React.ReactNode }) {
    {children} - {pathname.split('/')[1] !== 'settings' && } + {pathname.split('/')[1] !== 'settings' && } ) @@ -138,7 +138,7 @@ export function Sidebar({ children }: { children: React.ReactNode }) { } } -export const ProfileLinks = () => { +export const ProfileLinks = ({ className }: { className?: string }) => { const { me } = useCurrentUser() const logout = useApiMutation('logout', { @@ -151,7 +151,7 @@ export const ProfileLinks = () => { }) return ( -
    +
    @@ -161,8 +161,8 @@ export const ProfileLinks = () => { SSH Keys - logout.mutate({})}> - Sign out + logout.mutate({})} className="lg+:hidden"> + Sign Out
    @@ -246,6 +246,7 @@ export const NavButtonItem = (props: { onClick: () => void children: React.ReactNode disabled?: boolean + className?: string }) => (
  • @@ -133,21 +134,3 @@ export function TopBar({ children }: { children: React.ReactNode }) {
  • ) } - -const Menu12Icon = ({ className }: { className: string }) => ( - - - -) diff --git a/package-lock.json b/package-lock.json index b9cfd118a6..65b5df72a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "@floating-ui/react": "^0.26.9", "@headlessui/react": "^1.7.18", - "@oxide/design-system": "^1.2.10", + "@oxide/design-system": "^1.3.0", "@oxide/identicon": "0.0.4", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-dialog": "^1.0.5", @@ -2067,9 +2067,9 @@ "dev": true }, "node_modules/@oxide/design-system": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@oxide/design-system/-/design-system-1.2.14.tgz", - "integrity": "sha512-EXTCESKXxzCyAiH9dmIu3jLYOuCF413ATTiF8AeVsVfj5jhZx10VCd/HGqbt8+h0JH7ixppdFQB7v6+cMTvb/w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@oxide/design-system/-/design-system-1.3.0.tgz", + "integrity": "sha512-++uhslGzTyeN3YK9F4Ws6XG549Ml3nPdMtvX1eBhGCpNR1RHq83ErvwviziojTraCbyOH2x7qW8DOfpXPtrH5Q==", "dependencies": { "@figma-export/output-components-as-svgr": "^4.7.0", "@floating-ui/react": "^0.25.1", @@ -22012,9 +22012,9 @@ "dev": true }, "@oxide/design-system": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@oxide/design-system/-/design-system-1.2.14.tgz", - "integrity": "sha512-EXTCESKXxzCyAiH9dmIu3jLYOuCF413ATTiF8AeVsVfj5jhZx10VCd/HGqbt8+h0JH7ixppdFQB7v6+cMTvb/w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@oxide/design-system/-/design-system-1.3.0.tgz", + "integrity": "sha512-++uhslGzTyeN3YK9F4Ws6XG549Ml3nPdMtvX1eBhGCpNR1RHq83ErvwviziojTraCbyOH2x7qW8DOfpXPtrH5Q==", "requires": { "@figma-export/output-components-as-svgr": "^4.7.0", "@floating-ui/react": "^0.25.1", diff --git a/package.json b/package.json index d0d419ae30..0a3f94832c 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "dependencies": { "@floating-ui/react": "^0.26.9", "@headlessui/react": "^1.7.18", - "@oxide/design-system": "^1.2.10", + "@oxide/design-system": "^1.3.0", "@oxide/identicon": "0.0.4", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-dialog": "^1.0.5", From c8945db589ec58c570c1a4991c6d4e616698c3f4 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Wed, 27 Mar 2024 11:57:06 +0000 Subject: [PATCH 13/17] Login page inbetween breakpoint fixes --- app/ui/styles/components/login-page.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ui/styles/components/login-page.css b/app/ui/styles/components/login-page.css index 60b05b3246..ca7f1f265e 100644 --- a/app/ui/styles/components/login-page.css +++ b/app/ui/styles/components/login-page.css @@ -7,7 +7,7 @@ */ .hero-rack { - @apply absolute left-[calc(50%+40px)] top-[15%] h-[115%] max-h-[1080px] -translate-x-1/2 object-contain md:-mr-10; + @apply absolute left-[calc(50%+40px)] top-[15%] h-[115%] max-h-[1080px] -translate-x-1/2 object-contain md-:left-[calc(50%)]; } .hero-rack-wrapper { @@ -28,7 +28,7 @@ background: linear-gradient(90deg, rgba(42, 46, 49, 0.7) 12.5%, rgba(8, 15, 17, 1) 100%); } -@media (max-width: 767px) { +@media (max-width: 639px) { .layout { background: radial-gradient( 200% 100% at 50% 100%, From dc50b8127e4e8d2f74600b845e5b7b6f1b95a772 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Wed, 27 Mar 2024 12:36:04 +0000 Subject: [PATCH 14/17] Bump commit From e14fbfb5c46e9e91129ba2de63ebca18b830ba08 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Wed, 27 Mar 2024 16:25:18 -0500 Subject: [PATCH 15/17] let's just uh scroll a little farther --- test/e2e/z-index.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/z-index.e2e.ts b/test/e2e/z-index.e2e.ts index 7abcb9baa7..e2f637e523 100644 --- a/test/e2e/z-index.e2e.ts +++ b/test/e2e/z-index.e2e.ts @@ -27,7 +27,7 @@ test('Dropdown content can scroll off page and doesn’t hide TopBar', async ({ // scroll the page down just enough that the button and the top item are off // screen, but the bottom item is not - await page.mouse.wheel(0, 480) + await page.mouse.wheel(0, 485) // if we don't do this, the test doesn't wait long enough for the following // assertions to become true From 87a1049b9e0aea022f410fac5a121c07b86caaca Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 28 Jun 2024 15:37:06 -0500 Subject: [PATCH 16/17] format, lint --- app/components/Sidebar.tsx | 5 ++--- app/layouts/helpers.tsx | 2 +- app/pages/project/instances/instance/SerialConsolePage.tsx | 2 +- app/pages/project/instances/instance/tabs/MetricsTab.tsx | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx index f8dd84f4c2..d99f81e6f2 100644 --- a/app/components/Sidebar.tsx +++ b/app/components/Sidebar.tsx @@ -223,12 +223,11 @@ export const NavButtonItem = (props: { }) => (